如果有人问你,秘密数据是如何从 AWS Secrets Manager 流传到正在运行的 Kubernetes Pod 中的,你能自信地解释清楚吗?

存储这些秘密数据本身并不复杂。但如何处理密钥的轮换、过时的环境变量问题,以及 Pod 读取的数据与 AWS 实际存储的数据之间的差异,这些问题常常让许多工程师感到困惑。

在本指南中,你将构建一个从 AWS Secrets Manager 到 Kubernetes Pod 的完整秘密数据传输流程。你会使用 Terraform 来配置基础设施,通过 External Secrets Operator 来同步秘密数据,并运行一个示例应用程序,该程序会以两种不同的方式来获取这些秘密信息:一种是通过环境变量,另一种则是通过挂载的卷。

学习完本指南后,你将能够:

  • 了解从密钥存储库到 Pod 的整个数据传输架构

  • 在大约 15 分钟内完成本地实验环境的搭建

  • 理解为什么在密钥轮换后环境变量会变得过时,而挂载的秘密文件却能保持最新状态

  • 将这种解决方案应用到基于 Amazon Elastic Kubernetes Service 且使用 OpenID Connect 实现 CI/CD 的环境中

  • 排查最常见的故障问题

下图展示了秘密数据从 AWS Secrets Manager 经过 External Secrets Operator 流入 Kubernetes Secret,然后再分为在 Pod 启动时设置的环境变量,以及会在 60 秒内更新内容的挂载卷。

秘密数据传输流程示意图:从 AWS Secrets Manager 到 Kubernetes Secret,再分为环境变量和挂载卷

目录

先决条件

在开始之前,请确保您已经安装并配置了以下工具。

对于本地实验室环境:

  • 一个可以访问AWS Secrets Manager的AWS账户

  • 已安装并配置了AWS CLI。运行aws configure,然后输入您的访问密钥、秘密密钥、默认区域以及输出格式。这些凭据需要具备在AWS Secrets Manager中读写秘件的权限。

  • 已安装kubectl。对于Microk8s环境,在安装完成后运行microk8s kubectl config view --raw > ~/.kube/config,以便将kubectl连接到您的本地Kubernetes集群。

  • 已安装Terraform

  • 已安装Helm

  • 已安装Docker

  • 拥有一个本地Kubernetes集群:本实验环境支持Microk8s和kind。如果您还没有安装这些工具,请先参考Microk8s安装指南再进行操作。

对于与Amazon Elastic Kubernetes Service相关的部分:

  • 一个可以创建或管理的Amazon Elastic Kubernetes Service集群

  • 一个可用于配置工作流程和秘件的GitHub仓库

实验用仓库提供了两种部署方式:一种是本地路径,适用于快速学习;另一种是Amazon Elastic Kubernetes Service路径,适用于类似生产环境的设置。每种部署方式所需的具体命令都记录在仓库的docs/DEPLOY-LOCAL.mddocs/DEPLOY-EKS.md文件中。

如何理解秘件流转机制

在运行任何命令之前,您需要先了解各个组件是如何相互连接的。

整个流程分为四个阶段:

  1. 开发人员或自动化系统会更新AWS Secrets Manager中的秘件内容。

  2. External Secrets Operator会按照预定时间间隔检查AWS Secrets Manager,并据此创建或更新Kubernetes Secret。

  3. 您的Pod会读取这些Kubernetes Secret中的数据。

  4. 在秘件更新过程中,两种使用方式会表现出不同的行为。

一张逐步展示秘件流转四个阶段的流程图

External Secrets Operator的同步机制

External Secrets Operator会读取一种名为ExternalSecret的自定义Kubernetes资源。该资源会向Operator提供以下三项信息:

  • 应该连接到哪个秘件存储库

  • 需要创建或更新哪个Kubernetes Secret

  • 更新频率是多少

在这个实验中,ExternalSecret会创建一个名为myapp-database-creds的Kubernetes Secret。此外,该工具还会添加一个模板注释,当Secret的内容发生变更时,这个注释会触发Pod的重启。

应用程序如何使用这些Secrets

示例应用程序提供了三个终端点,因此你可以随时验证其运行行为。

  • /secrets/env用于显示Pod能够访问哪些环境变量

  • /secrets/volume用于展示挂载在Pod中的Secret目录中包含哪些文件

  • /secrets/compare可以对比这两组信息,并报告是否检测到了Secret内容的更新

该应用程序会检查以下四个键值对:DB_USERNAMEDB_PASSWORDDB_HOST以及DB_PORT

如何运行本地实验环境

本地实验环境能够让你快速掌握相关知识。你可以在不等待云部署完成的情况下,直接观察整个流程并测试Secret内容的更新机制。

步骤1:克隆仓库

git clone https://github.com/Osomudeya/k8s-secret-lab
cd k8s-secret-lab

步骤2:运行启动脚本

bash spinup.sh

该脚本会要求你选择适合的本地集群类型。根据你所安装的环境,可以选择Microk8s或kind。脚本会通过Helm安装External Secrets Operator,应用Terraform配置文件,然后部署示例应用程序。

如果脚本在运行过程中出现错误,请先查看docs/TROUBLESHOOTING.md中的解决方法。最常见的故障原因包括缺少AWS认证信息、kubeconfig配置错误,或者未启用Microk8s的存储插件。

重要提示:请运行实验界的用户界面

该实验环境还提供了独立的指导性教程界面,你可以在笔记本电脑上使用这个界面。这个界面并不是集群内部的应用程序,而是位于lab-ui/目录下的一个基于React开发的检查清单工具,它会在你进行实验的过程中,一步步引导你了解各个概念和操作步骤。

要启动这个界面,请打开另一个终端窗口,然后运行以下命令:

cd lab-ui && npm install && npm run dev

在终端中运行npm run dev时显示的界面截图

之后,打开http://localhost:5173。你会看到一个分模块编写的指南,其中详细介绍了从使用外部Secrets到更新Secret内容,再到实现CI/CD流程的整个过程。

实验界的用户界面截图,这是一个伴随实验进程运行的指导性工具,会一步步引导你了解各个概念和操作步骤。

请保持这个终端处于运行状态,以便与实验环境同步使用。实验室用户界面以及集群内的应用程序(地址为localhost:3000)是两个不同的组件:用户界面会指导您完成相关操作,而应用程序则会实时显示各种秘密信息。

步骤3:访问应用程序

当实验完成后,请将相关服务端口进行转发。

kubectl port-forward svc/myapp 3000:80 -n default

打开http://localhost:3000,您会看到一个表格,其中列出了每一项秘密信息,以及这些环境变量的值是否与对应的存储数据相匹配。

在localhost:3000上运行的应用程序的截图。表格中的每一行都会显示“Match ✓”字样

步骤4:验证秘密信息是否匹配

直接在终端中运行比较命令进行验证。

curl -s http://localhost:3000/secrets/compare | python3 -m json.tool

如果一切正常,响应结果中会包含"all_match": true这一字段。

如何检查ExternalSecret及应用程序

此时实验环境已经启动运行。接下来,您需要仔细查看相关配置文件,以便了解各个组件的具体功能。

步骤1:阅读ExternalSecret配置文件

打开k8s/aws/external-secret.yaml文件,重点关注以下四个字段:

  • refreshInterval:操作员会每隔多久查询一次AWS Secrets Manager中的数据

  • secretStoreRef:操作员将使用哪个存储系统进行身份验证

  • target:要创建的Kubernetes Secret的名称

  • data:AWS Secrets Manager中的JSON键与Kubernetes Secret键之间的对应关系

在这个实验环境中,这种对应关系的具体表现如下:

spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: myapp-database-creds
    creationPolicy: Owner
  data:
    - secretKey: DB_USERNAME
      remoteRef:
        key: prod/myapp/database
        property: username

property字段指明了操作员应该从AWS Secrets Manager中的哪个JSON键中提取数据。如果您的秘密信息是一个JSON对象,那么每个字段都会在配置文件中单独出现。

在继续下一步之前,有两个字段值得特别关注。creationPolicy: Owner表示操作员拥有自己创建的Kubernetes Secret;如果删除了ExternalSecret,对应的Kubernetes Secret也会被自动删除。ClusterSecretStore是一种集群范围内的存储方式,这意味着集群内的任何命名空间都可以使用它。而普通的SecretStore则是命名空间范围的。对于这个实验来说,使用集群范围的存储方式更为合适,因为这样可以让整个配置过程更加简单明了。

步骤 2:阅读部署配置文件

打开文件 k8s/aws/deployment.yaml。你需要关注其中的两个部分:envFromvolumeMounts

envFrom:
  - secretRef:
      name: myapp-database-creds

volumeMounts:
  - name: db-secret-vol
    mountPath: /etc/secrets
    readOnly: true

这两段代码都是从同一个 Kubernetes Secret 中获取数据的,这个 Secret 的名称是 myapp-database-credsenvFrom 部分会在 Pod 启动时将其中的所有键值对作为环境变量注入到 Pod 中;而 volumeMounts 部分则会将这些数据以文件的形式挂载到 /etc/secrets 目录下。

这就是这个实验的核心所在:虽然这两段代码都从同一个数据源获取信息,但当该数据源发生变化时,它们的行为却会有所不同。

步骤 3:阅读应用程序的比较逻辑代码

打开文件 app/server.js。这个程序会从 process.env 中读取环境变量,同时也会从 /etc/secrets/ 目录下读取已挂载的 Secret 文件中的数据,然后对比这两组数据,计算出每个键值对是否匹配,以及整体上是否存在不匹配的情况。

当环境变量中的键值与挂载文件中的内容不一致时,/secrets/compare 端点会将 rotation Detected: true 这一标志设置为真。

如何测试 Secret 的轮换机制

Secret 的轮换机制往往是开发团队面临的一大难题。通过这个实验,你可以清楚地了解这一机制的工作原理,从而更有信心地解决相关问题。

轮换机制的工作原理

当一个 Pod 启动时,Kubernetes 会提供两种方式让它获取 Secret 中的数据。

第一种方式是通过环境变量。可以把这些环境变量想象成容器启动时贴在容器墙上的便条——这些值只会在启动时被写入一次,之后就不会再发生变化了。即使 AWS 中的 Secret 数据在十分钟后发生了更新,那些便条上显示的仍然是旧值。因为容器无法看到这些更新。

第二种方式是通过卷挂载。可以把这种方式想象成一个可以被其他人远程更新的共享文件夹。Kubernetes 会在容器内部创建一个文件夹,并将 Secret 的数据保存在这个文件夹中的文件里。当 AWS 中的 Secret 数据发生变化时,ESO 会将其同步到 Kubernetes 中,然后 kubelet 会在大约 60 秒内更新该文件。因此,每当容器需要获取 Secret 数据时,它都会读取到最新的值。

同样的 Secret 数据,通过这两种不同的方式获取后,结果就会截然不同:一种方式得到的数据会过时,而另一种方式得到的数据则始终是最新的。

问题就出在这里:如果你的应用程序是从环境变量中获取数据库密码的,那么当 AWS 中的密码发生变化时,由于环境变量的值仍然保持不变,应用程序就无法正常连接数据库,从而导致连接失败。

这种差异并不是一个错误,而是Linux进程模型与kubelet的工作方式所决定的。理解这一点,意味着你真正掌握了Kubernetes中的秘密配置机制,而而不仅仅是了解它们的存在。

在实验中,你会观察到以下现象:

  • 旋转脚本会更新AWS中的秘密值

  • ESO会在几秒钟内将新值同步到Kubernetes系统中

  • 相应的卷文件也会自动更新

  • 然而环境变量在Pod重启之前仍然会保持旧的值

  • /secrets/compare这个接口会同时显示两个值,让你能够实时看到它们之间的差异

步骤1:确认实验环境已准备就绪

在开始实验之前,请确保你的Pod以及External Secrets Operator都在正常运行。

kubectl get pods -n external-secrets
kubectl get pods -n default

这两个命令都应该显示“Running”状态。

步骤2:运行旋转测试脚本

bash rotation/test-rotation.sh

该脚本会按以下顺序执行操作:

  1. 从位于/etc/secrets/DB_PASSWORD的卷中读取当前的DB_PASSWORD

  2. 再从环境变量中读取当前的DB_PASSWORD

  3. 使用put-secret-value命令将新密码更新到AWS Secrets Manager中

  4. 通过为ExternalSecret添加force-sync标记,强制立即进行同步操作

  5. 再次从卷中读取密码值

  6. 再次从环境变量中读取密码值

脚本运行完成后,卷中的密码值与环境变量中的密码值将会不同。

步骤3:使用比较接口进行验证

访问/secrets/compare接口并查看输出结果。

curl -s http://localhost:3000/secrets/compare | python3 -m json.tool

你将会看到类似以下的输出:

{
  "comparison": {
    "DB_PASSWORD": {
      "env": "old-password-value",
      "volume": "new-password-value",
      "match": false
    }
  },
  "all_match": false,
  "rotation Detected": true,
  "message": "Volume has new value; env still has old value."
}

密码值不一致:卷文件中的密码已更新为新值,但环境变量中仍保留着Pod启动时的旧值。

步骤4:重启部署以使环境变量同步

环境变量的值不会自动更新,因此需要重新启动Pod,这样新的容器才会使用到已更新的Kubernetes秘密配置。

kubectl rollout restart deployment/myapp -n default
kubectl rollout status deployment/myapp -n default

然后再次访问 /secrets/compare。此时所有行都应该显示 “all_match: true”。

在滚动重启之后,新的Pod会获取到最新的环境变量,因此所有的键值对都会匹配。” height=

如何使用Reloader自动执行重启操作

如果你不希望每次轮询后都手动重新启动部署任务,你可以安装 Stakater Reloader。该工具会监控 Deployment 对象上的相关注释,当引用的Kubernetes Secret发生变化时,它会自动触发滚动重启操作。新的Pod会使用最新的环境变量启动,而旧的Pod则会被顺利终止。该工具的官方文档中包含了安装步骤。

如何在外部Secrets Operator与CSI Driver之间进行选择

在将外部秘密数据导入Kubernetes系统时,主要有两种方法可供选择:外部Secrets Operator和Secrets Store CSI Driver

这两种方法都可以将云端的秘密数据导入Pod中,但它们的实现方式有所不同。以下是两者之间的简单对比:

功能 外部Secrets Operator Secrets Store CSI Driver
是否会创建Kubernetes Secret 默认情况下不会
是否支持 envFrom 配置 支持 不支持(仅提供变通方案)
秘密数据是否存储在etcd中 会(以base64格式存储) 如果不执行同步操作,则不会存储在etcd中
秘密数据的更新机制 外部Secrets Operator会更新Secret,然后Reloader会重启Pod CSI Driver可以直接修改卷文件中的秘密数据
适用场景 适合大多数团队使用,尤其适用于多云环境或需要支持环境变量配置的场景 适用于那些禁止将秘密数据存储在etcd中的安全策略环境

本实验选择使用外部Secrets Operator,原因有两条:首先,它能够生成标准的Kubernetes Secret对象,这样你的应用程序和部署流程就能符合Kubernetes的标准规范;其次,同时使用 envFrom 配置以及指向同一Secret的卷挂载点,可以让我们更方便地观察两种方法在重启过程中的表现差异。

当你的安全团队禁止通过Kubernetes Secret将秘密数据存储在etcd中时,可以选择使用CSI Driver。这种驱动方式会直接将秘密数据导入Pod的文件系统,而不会生成Kubernetes Secret对象。不过这样一来,你就无法使用 envFrom 配置了。

如何在Amazon Elastic Kubernetes Service上部署这种方案

这个本地实验室非常适合用于学习。Amazon Elastic Kubernetes Service的相关配置为学习环境提供了类似生产环境的要素:为操作人员设置了基于IAM角色的权限机制,为应用程序配备了负载均衡器,同时还建立了完整的CI/CD工作流程。

步骤1:准备Terraform及OpenID Connect访问权限

该仓库中包含了一份针对基于OpenID Connect的访问机制的一次性设置指南,这些指导信息位于terraform/github-oidc文件夹中。请在该文件夹中运行以下命令。

cd terraform/github-oidc
terraform init
terraform plan -var="github_repo=YOUR_ORG/YOUR_REPO"
terraform apply -var="github_repo=YOUR_ORG/YOUR_REPO"
terraform output role_arn

从输出结果中复制role ARN,您在下一步会需要用到它。

步骤2:设置所需的环境变量

Amazon Elastic Kubernetes Service的启动流程需要您的GitHub Actions角色ARN,这样Terraform才能为CI/CD运行器授予访问集群的权限。

要获取您的AWS账户ID,请运行以下命令:

aws sts get-caller-identity --query Account --output text

然后将得到的账户ID作为变量值进行设置,其中ACCOUNT应替换为该命令返回的实际数值。

export GITHUB_actions_ROLE_ARN=arn:aws:iam::ACCOUNT:role/your-github-oidc-role

步骤3:运行Amazon Elastic Kubernetes Service的启动脚本

bash spinup.sh --cluster eks

当脚本执行完成后,它会输出应用程序的URL。请在浏览器中打开该URL,确认您看到的密钥信息与在本地环境中看到的一致,所有密钥都应该显示Match ✓状态。

步骤4:在部署后的应用程序上测试密钥轮换机制

确认系统运行正常后,请按照与在本地环境中相同的方式运行密钥轮换测试。

bash rotation/test-rotation.sh

随后,使用/secrets/compare命令来检查Amazon Elastic Kubernetes Service负载均衡器上的密钥信息,从而验证云环境中的密钥轮换机制是否正常工作。

⚠️ 费用提示: Amazon Elastic Kubernetes Service的每小时使用成本约为0.16美元。实验结束后,请从仓库根目录运行bash teardown.sh命令,以释放所有AWS资源并停止费用生成。

在ALB上运行的应用程序的截图,所有密钥均匹配成功

如何在不存储AWS凭证的情况下配置GitHub Actions

典型的CI/CD设置会将AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY保存在GitHub仓库的秘密信息中。这些密钥永远不会被更新,任何拥有仓库访问权限的人都可以查看它们。当有人离开团队时,就需要撤销这些密钥并重新配置所有的工作流程。

OpenID Connect能够完全解决这个问题。

OpenID Connect在GitHub Actions中的工作原理

GitHub可以为每次工作流程的运行生成一个临时令牌。这个令牌会包含仓库信息、分支名称以及工作流程的名称。你可以在AWS中创建一个IAM角色,其信任策略规定:只接受来自特定GitHub仓库和分支的请求。GitHub Actions的执行器会使用AssumeRoleWithWebIdentity函数将这个临时令牌兑换成临时的AWS凭证。因此,没有任何长期有效的密钥会被保存在任何地方。

GitHub Actions部署到EKS时的完整OIDC认证流程——从生成JWT令牌,到通过AssumeRoleWithWebIdentity获取临时凭证,再到读取kubeconfig文件并最终执行kubectl apply命令。

步骤1:使用Terraform创建IAM角色

terraform/github-oidc这个文件夹会帮你生成OpenID Connect提供者以及相应的IAM角色。在之前设置Amazon Elastic Kubernetes Service的过程中,你已经使用过这个脚本了。你只需要保存该角色的ARN值即可。

步骤2:将角色ARN添加到GitHub仓库的秘密信息中

在你的GitHub仓库中,按照以下步骤操作:

  1. 进入“设置” → “秘密与变量” → “Actions”

  2. 点击“新建仓库秘密”

  3. 将其命名为AWS_ROLE_ARN

  4. 将Terraform输出中得到的角色ARN值粘贴到这里

这就是你需要保存的唯一秘密信息。角色ARN并不属于敏感数据,它只是一种标识符,并非凭证。

步骤3:配置Terraform的状态管理机制

为了确保CI/CD流程在每次运行时都能保持一致性,Terraform需要一个共享的状态存储后端。在这个实验环境中,我们使用Amazon S3 bucket来存储Terraform的状态数据,并通过Amazon DynamoDB表来实现状态锁机制。仓库中提供的Amazon Elastic Kubernetes Service部署指南已经详细介绍了后端的配置方法。

步骤4:将代码推送到主分支并运行工作流程

在首次完成配置之后,每次向main分支提交代码都会触发CI/CD流程。仓库中包含了专门用于处理Terraform基础设施变更以及应用程序部署变更的工作流程文件。一旦你的应用程序可以正常访问,就可以使用/secrets/compare命令来验证实际环境中的密钥轮换机制是否正常工作。

如何排查最常见的故障

以下是部分常见症状及其解决方法。

)

症状 最可能的原因 解决方法
ExternalSecret未同步 凭据缺失或存储引用错误 确认操作员能够访问AWS Secrets Manager,并且secretStoreRef指向正确的存储位置
Pod处于Pending状态 本地集群的存储配置未完成 对于Microk8s,需要启用存储插件
轮换后环境变量与卷仍然不匹配 虽然进行了轮换,但Pod并未重新启动 运行kubectl rollout restart或安装Reloader工具
CRD版本与API版本不一致 ESO版本与配置文件中的apiVersion不匹配 检查ClusterSecretStoreExternalSecretapiVersion是否与你安装的ESO版本一致
Amazon Elastic Kubernetes Service节点组无法加入集群 节点的网络配置或IAM权限设置错误 调整网络路由并检查节点的IAM策略

如何检查操作员及ExternalSecret的状态

当发现数据同步问题时,可以先使用以下两个命令进行排查。

# 检查ExternalSecret的状态
kubectl describe externalsecret app-db-secret -n default

# 查看ESO操作员的日志
kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secrets

ExternalSecret资源的状态信息通常能帮助你准确判断出现问题的原因。

如何从应用程序端验证轮换机制

在调试轮换功能时,不要仅依赖Kubernetes资源的状态信息。可以使用/secrets/compare接口来查看运行中的应用程序实际检测到的数据。该接口能告诉你环境变量与卷是否匹配,以及是否检测到了轮换操作。这些数据才是判断应用程序行为的真实依据。

总结

现在你已经掌握了一套完整的流程:通过Terraform和External Secrets Operator将AWS Secrets Manager中的秘密信息导入Kubernetes Pod中。你还亲自测试了秘密信息的轮换机制,并观察到了关键现象:在kubelet的同步周期内,挂载的秘密文件会得到更新,而环境变量则会在Pod重新启动之前保持不变。这一观察结果能够解释很多生产环境中出现的问题。

最后,你们了解了如何将同样的设计应用到基于 OpenID Connect 的持续集成/持续部署机制的 Amazon Elastic Kubernetes Service 上;同时,我们也为大家准备了一份用于排查各类常见故障的检查清单。

该实验仓库的地址为github.com/Osomudeya/k8s-secret-lab。如果您已经在本地环境中进行了相关实验,那么接下来应该按照仓库中提供的分阶段学习路径来执行第4步和第5步:首先在Microk8s环境下测试CSI驱动程序的相关功能,然后按照EKS的设置步骤来验证该流程在实际的CI/CD工作流中的运行效果,同时注意不要将任何认证信息存储在GitHub上。这两个步骤的具体操作方法都在仓库中有详细说明,完成每一步所需的时间都不到30分钟。

如果这些内容对您有所帮助,请给这个仓库添加星标,并将其分享给那些正在学习Kubernetes的人。

我会每周发布关于真实生产环境中出现的问题以及工程师们是如何解决这些问题的详细分析报告,这些内容并非教程,而是真实的故障案例。
订阅我的通讯吧

Comments are closed.