
第66天的 DevOps,第16天的 Azure(微软云)。今天的两个任务都指向同一个问题:名字必须完全正确。我把 PersistentVolumeClaim(持久卷声明)建错了名字,结果 Pod 永远调不上来——没有容器,也就没有日志可看。Azure 存储账户的名字要求全局唯一,因为它会被当作主机名来用。
一个 Kubernetes 任务,一个 Azure 任务。在持久卷上跑 MySQL,把凭证放到 Secrets 里,然后创建一个私有 blob 容器。任务来自 KodeKloud Engineer 平台。
任务要求:250Mi 的 PersistentVolume(持久卷)和对应的 claim,四个 Secret 分别存 root 密码、应用用户名和密码、以及数据库名,一个 Deployment 从这些 Secret 读取所有配置,再加一个 NodePort Service 映射到 30007。
apiVersion: v1
kind: PersistentVolume
metadata: name: mysql-pv labels: type: local
spec: storageClassName: manual capacity: storage: 250Mi accessModes: - ReadWriteOnce hostPath: path: "/mnt/dba"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: name: mysql-pv-claim
spec: storageClassName: manual accessModes: - ReadWriteOnce resources: requests: storage: 250Mi
kubectl apply -f pv.yaml
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS
mysql-pv 250Mi RWO Retain Available manual
手动创建的 PV 默认的回收策略是 Retain,而不是动态provisioning时的 Delete。
kubectl create secret generic mysql-user-pass \ --from-literal='username=kodekloud_tim' --from-literal='password=8FmzjvFU6S'
kubectl create secret generic mysql-root-pass --from-literal='password=YUIidhb667'
kubectl create secret generic mysql-db-url --from-literal='database=kodekloud_db6'
kubectl get secrets
NAME TYPE DATA AGE
mysql-db-url Opaque 1 5s
mysql-root-pass Opaque 1 42s
mysql-user-pass Opaque 2 81s
用户凭证的 Secret 包含两个 key,所以它的 DATA 列显示 2。后续如果需要轮换应用密码,只需要动一个对象,而不是三个。
containers: - image: mysql:8.0 name: mysql-container env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: mysql-root-pass key: password - name: MYSQL_DATABASE valueFrom: secretKeyRef: name: mysql-db-url key: database - name: MYSQL_USER valueFrom: secretKeyRef: name: mysql-user-pass key: username - name: MYSQL_PASSWORD valueFrom: secretKeyRef: name: mysql-user-pass key: password volumeMounts: - name: mysql-pvc mountPath: /var/lib/mysql
/var/lib/mysql 不是随意选的。官方镜像的文档明确说这是"MySQL 默认写入数据文件的位置"。
Warning FailedScheduling 0/1 nodes are available: persistentvolumeclaim "mysql-pv-claim" not found.
Warning FailedScheduling 0/1 nodes are available: pod has unbound immediate PersistentVolumeClaims.
我把 claim 建成了 volume 的名字,而不是它自己的名字。所以 kubectl get pvc 列出来的是 mysql-pv,但 Deployment 找的是 mysql-pv-claim。
关键在于这个问题怎么体现出来。容器都没起来,所以没有日志。也没有镜像拉取失败,所以 Pod 状态里也看不出什么。Pod 就是单纯没被调度,唯一能找到原因的地方是 kubectl describe pod 的 Events 部分。这跟容器启动后崩溃的排查路径完全不同,第一步看的地方也不一样。
两条消息也是不同的。第一条说明 claim 不存在。第二条说明 claim 存在但还没绑定到任何卷——如果 StorageClass 或容量不匹配,看到的就是这条。
kubectl logs <pod>
[Note] [Entrypoint]: Initializing database files
[Note] [Entrypoint]: Starting temporary server
[Note] [Entrypoint]: Creating database kodekloud_db6
[Note] [Entrypoint]: Creating user kodekloud_tim
[Note] [Entrypoint]: Giving user kodekloud_tim access to schema kodekloud_db6
[Note] [Entrypoint]: Stopping temporary server
[Note] [Entrypoint]: MySQL init process done. Ready for start up.
镜像先启动一个临时服务器,应用环境变量,关闭它,再启动正式的。这套流程只在数据目录为空时执行,所以第二个 Pod 挂载同一个 claim 的话,这些初始化步骤就不会跑了。
kubectl exec -it <pod> -- mysql -u root -p
kubectl exec -it <pod> -- mysql -u kodekloud_tim -p
两个账户都能登录,这验证的是凭证,而不是 Pod 的 status 字段。
kubectl describe pod 很谨慎:
MYSQL_ROOT_PASSWORD: <set to the key 'password' in secret 'mysql-root-pass'> Optional: false
但容器内容不这么认为:
kubectl exec -it <pod> -- env | grep MYSQL
MYSQL_PASSWORD=8FmzjvFU6S
MYSQL_ROOT_PASSWORD=YUIidhb667
MYSQL_DATABASE=kodekloud_db6
MYSQL_USER=kodekloud_tim
把 Secret 放进环境变量,意味着任何能在容器里执行命令的东西都能读到它,/proc/<pid>/environ 也一样。Secret 对象在传输过程中尽了责,但到了目的地就不再保护任何东西了。
第62天用的是把 Secret 挂载成文件的方式,那样值不会出现在环境变量里,kubelet 还能在原地更新它。环境变量这种形式恰恰相反:改了 Secret"容器不重启就看不到"。
还要注意 MYSQL_MAJOR 和 MYSQL_VERSION 也在上面的输出里,来自镜像本身。对着环境变量 grep 分辨不出哪些是你注入的,哪些是镜像自带的。
两个对象都要设置 storageClassName,并且理解省略它和设为空不是一回事。文档区分了三种情况。PVC 上写 storageClassName: "" 意思是"请求一个没有 class 的 PV"。完全不写 storageClassName 的 PVC 在集群里的行为取决于 DefaultStorageClass 准入插件是否开启——如果存在默认 class,"所有没有 storageClassName 的 PVC 只能绑定到那个默认 class 的 PV 上"。
所以在有默认 StorageClass 的集群里,手动创建的没有 class 的 PV 和省略了字段的 PVC 永远绑不到一起。claim 会被动态 provision 到别处,PV 则永远处于 Available 状态。两边都写 manual——也就是任务要求的写法——就绕开了这个问题。
这一点在 k3s 上尤其重要,它默认自带 Local Path Provisioner。这个 local-path class 有没有被标注为默认,k3s 文档没写,它自己的例子也是显式指定 class 而不是依赖默认。所以我只能确认 provisioner 存在,不能从文档里确认它是默认的。
hostPath 只适合实验室环境。Kubernetes 官方文档的示例在代码注释里就写了警告,"仅供单节点测试使用;多节点集群下无法工作",Volumes 页面的说法更直白:"使用 hostPath 卷类型存在许多安全风险。如果可以避免,你应该不要用 hostPath 卷"。
给数据库设置资源。这个 Pod 是 BestEffort 级别,因为没有设置任何 requests 或 limits,所以节点内存紧张时它会第一个被驱逐。
还要检查 Service 实际连通到了什么。我的 Service 监听 80、目标 80,describe svc 显示 Endpoints: 10.22.0.9:80,但 MySQL 绑定的是 3306。任务指定了这些数字,所以能通过,Service 实际上在往一个关着的端口发流量。这是第64天的反方向:那天是我修了掩盖问题的那个东西才发现 targetPort 错了,而这里没有任何东西去掩盖它。
任务要求:一个存储账户 xfusionst744158879,里面建一个私有容器 xfusion-blob-744158879。
| Storage account | Container | |
|---|---|---|
| Uniqueness | Across Azure | Within the account |
| Length | 3 to 24 | 3 to 63 |
| Characters | Lowercase letters and digits | Lowercase, digits, hyphens |
| Hyphens | Not allowed | Allowed, not consecutive, each between alphanumerics |
微软的文档:"存储账户名称长度必须在 3 到 24 个字符之间,只能包含数字和小写字母",以及"存储账户名称在 Azure 内必须唯一"。
任务给的这两个名字看起来不一致,但各自都是其资源类型的唯一合法形式。
Azure 全局唯一是 DNS 的要求,因为账户名会变成子域名:
xfusionst744158879.blob.core.windows.net
所以实验室里才要加一串看起来像随机数的后缀。光是 xfusionst 的话,全球某个你看不见的订阅里早就有人用了。
之前我一直把这个失败叫做 StorageAccountAlreadyTaken,这不是微软文档里写的错误码。账户创建是控制面操作,名字检查返回的原因是 AlreadyExists;数据面错误表里有 AccountAlreadyExists,状态码 409。两个都不是我之前写的拼法。
az storage account create -g $RG -n $SA --sku Standard_LRS --kind StorageV2 KEY=$(az storage account keys list -g $RG -n $SA --query "[0].value" -o tsv)
az storage container create --name $CONTAINER --account-name $SA --account-key $KEY
az storage account show -g $RG -n $SA --query "{AllowBlobPublic:allowBlobPublicAccess}"
az storage container show --name $CONTAINER --account-name $SA --account-key $KEY \ --query "{PublicAccess:properties.publicAccess}"
"AllowBlobPublic": false
"PublicAccess": null
两层开关相互独立,任一层阻止就够了。账户级别的默认值文档里说"该属性的默认解释是 false",interpretation 这个词是有实际含义的:存储的值可能是 null,而微软自己的修复脚本把 null 和 true 同样对待。
容器级别,私有意味着没有值,而不是有个叫私有的值:"如果响应中没有返回这个头,说明容器只对账户所有者私有"。
值得知道的是这个状态在 Azure 各处的叫法有五种。门户叫 Private,PowerShell 叫 Off,CLI 接受 blob, container, off,数据面省略这个头,Azure Resource Manager 返回字符串 "None"。唯一一致的是:没有任何 API 把 "private" 当作值,所以检查 publicAccess == 'private' 在任何地方都匹配不到。
我用账户密钥创建容器,而不是登录的身份——这不是偷懒。微软说:"Owner、Contributor、Storage Account Contributor 等内置角色允许安全主体管理存储账户,但不通过 Microsoft Entra ID(微软身份识别服务)提供对该账户内 blob 数据的访问权限"。
所以 Owner 能建账户、读密钥、改配置,但用 --auth-mode login 列 blob 时还是会被拒绝。数据访问需要单独的角色:Storage Blob Data Owner、Contributor 或 Reader。
但这有个破坏整洁版说明的例外,我之前忽略了。Owner 和 Contributor 包含了 Microsoft.Storage/storageAccounts/listKeys/action,微软接下来说明了结果:拥有这个角色的用户"可以通过使用账户访问密钥进行共享密钥授权来访问存储账户中的数据"。在授权层面这层分离是真实的,但实践中是有漏洞的——除非把共享密钥访问关掉。
角色分配也需要时间,微软发布的数据不止一个:"最多30分钟传播"在一处提示里,同一页的另一处说"最多10分钟生效",管理组范围内的数据操作则需要12小时。任何引用这个时间的说法都需要标明来源。
优先使用身份而非密钥。文档记载账户密钥是512位,有两把,所以可以轮换着在线使用,而且"存储账户访问密钥提供对存储账户数据的完全访问权,以及生成 SAS 令牌的能力"。没有过期、没有身份,所以访问日志记录的是操作而不是人。微软的推荐顺序是:Entra ID 加数据角色、用户委托 SAS、最后才是密钥。
如果禁用了密钥,要知道还有什么也会停止工作。把 allowSharedKeyAccess 设为 false 意味着"Azure 存储拒绝所有后续使用账户访问密钥授权的请求",这包括服务 SAS 和账户 SAS,因为两者都用共享密钥签名。用户委托 SAS 还能用,因为它用 Entra ID 密钥签名。默认值是宽松的:"默认为 null,等同于 true"。
在放任何真实数据之前开启软删除,而且要查默认值而不是假设。Blob 软删除"通过 Azure 门户创建新存储账户时默认开启",但微软同样直接地说"通过 Azure CLI 创建新存储账户时不会开启"。这个账户是用 CLI 建的,所以没有软删除。容器软删除的默认值文档也没有明确说明。
修正一下我自己之前的冗余表。我之前把 LRS 写成"同一数据中心的三份副本"。托管磁盘文档确实说三份副本,但存储账户冗余页面不再给 LRS 的副本数了:它说"位于您选择的主要区域内的单个物理数据中心",可用性表述变成了"至少 99.999999999%(11个9)"。ZRS 是"三个或更多 Azure 可用区",不是恰好三个。如果某篇文章说某个存储账户有好几份副本,那它是走在文档前面的。
两个部分在名字值多少钱这个问题上给出了不同的答案。在 Kubernetes 里,名字错了只是损失一个永远调不上来的 Pod,事件里有清晰的信息等着你。在 Azure 里,存储账户名字是共享命名空间里的主机名,所以一个只是听起来合理的名字早就被别人占用了。
另一件今天带走的东西是环境变量输出。Secret 对象工作得完全符合设计,但进容器里三秒钟的 env 就把所有值明文打出来了。保护在传输过程中有效,到达目的地就结束了。
所以这是第66天的问题。在你的系统里,secret 在哪里不再被当作 secret 来处理,你的工具链里有没有什么能告诉你这条线在哪里?
第66天完成。还剩34天。