亚马逊AWS官方博客

EKS上有状态服务启用存储加密

1.背景

用户通过 Deployment、Replication Controller 可以方便地在 Kubernetes 中部署一套高可用、可扩展的分布式无状态服务。这类应用不在本地存储数据,通过简单的负载均衡策略可实现请求分发。Deployment、Replication Controller 是为无状态服务而设计的,它们中 Pod 的名称、主机名、存储都是不稳定的,且 Pod 的启动、销毁顺序随机,并不适合数据库这样的有状态应用。

为此,Kubernetes 推出了面向有状态服务的工作负载 StatefulSet,其管理的 Pod 具有如下特点:

  • 唯一性 – 对于包含 N 个副本的 StatefulSet,每个 Pod 会被分配一个 [0,N]范围内的唯一序号。
  • 顺序性 – StatefulSet 中 Pod 的启动、更新、销毁默认都是按顺序进行的。
  • 稳定的网络身份标识 – Pod 的主机名、DNS 地址不会随着 Pod 被重新调度而发生变化。
  • 稳定的持久化存储 – 当 Pod 被重新调度后,仍然能挂载原有的 PersistentVolume,保证了数据的完整性和一致性。

本文针对有状态服务业务场景的数据保护安全需求,旨在从EKS内部结合采用StatefulSet、Snapshot Controller 的方式实现有状态服务的存储加密启用,并在测试环境下验证了本方案的可行性。

2.场景案例

2.1 场景架构

业务场景:

该业务平台构建在AWS云上,服务客群主要为全国各医院,核心业务价值在于简化医院采购结算环节,帮助医院方降本增效。该平台通过结合医院,医院供应商提供的订单、结算单、发票信息,进行三单验证,并集成银行侧银企直连,进而实现医院的快速透明支付结算、有效提升采购效率。

部署架构:

之前客户在本地IDC的Kubernetes集群中除了部署了无状态的微服务应用外,还部署了有状态的中间件服务,包括Redis、Kafka、 MySQL,存储使用的是本地磁盘。由于自建Kubernetes集群的升级、管理、安全等各方面的运维工作复杂,需要耗费大量的运维资源;并且在Kubernetes集群中运行有状态的应用,特别是业务数据库,存在很大的潜在风险,因而在迁移至AWS云上的过程中,接受我方建议做了相应的架构优化及调整:

  • 自建Kubernetes调整为托管的Amazon EKS服务
  • 从Kubernetes集群中移除关系数据库MySQL,替换为托管的RDS Aurora服务
  • EKS中各业务组件采用无状态部署
  • EKS中保留Redis和Kafka等中间件,并采用有状态部署,持久化采用EBS
  • EKS中采用Amazon EFS作为共享存储,实现存储的安全高可用

架构示意图:

2.2 问题及解决方案

问题描述:

由于涉及到订单、结算单、发票等敏感数据的传输及存储,在业务上线后不久,客户希望加强云上数据安全保护,结合KMS 对持久化数据进行自动存储加密。之前有状态服务Redis、Kafka等在部署上线时所挂载的EBS 并未启用加密,由于EBS在初始未启用加密的情况下无法直接开启加密(见下面技术约束示意图),那么在对业务的影响降至最低的前提下如何实现EBS 存储加密的开启及业务的平滑迁移呢?

技术约束示意图:

初始未加密EBS无法直接启用加密,需对未加密EBS做快照,再将未加密快照还原成加密EBS,如下图所示

解决方案:

  • 方案一:需应用层做数据迁移,EKS中原Redis等有状态服务保持不动,采用StatefulSet方式新建Redis等有状态服务并启用EBS存储加密,在应用层做数据迁移(将敏感数据从原服务未加密EBS迁移至新建服务已加密EBS)后将中间件服务切换至新建Redis等有状态服务并下线原服务以实现有状态服务的存储加密;
  • 方案二:不涉及应用层数据迁移,从AWS控制台直接对EBS进行相关操作,将未加密EBS制作快照后还原成加密EBS,并将加密EBS静态挂载回正确的Redis等有状态服务;
  • 方案三:不涉及应用层数据迁移,从EKS内部进行相关操作,首先启用Snapshot Controller, 之后通过该Controller对未加密pvc制作快照后还原成加密pvc,新建Redis StatefulSet并挂载加密pvc,引流至新建Redis 测试一切正常后做服务迁移;

由于客户不希望应用层做数据迁移,以及从AWS控制台操作后如何正确静态挂载回对应的Pod 存在较大风险,最终采用了方案三进行了相应的测试及部署,并成功启用了EKS下EBS的存储加密。

3.部署与测试

接下来我们会按照上述方案三进行部署测试,整体的过程如下:

3.1 环境准备

安装AWS CLI, eksctl,kubectl 和 helm;

安装并配置 AWS CLI:https://docs.thinkwithwp.com/cli/latest/userguide/getting-started-install.html

安装eksctl:https://docs.thinkwithwp.com/eks/latest/userguide/eksctl.html

安装kubectl: https://docs.thinkwithwp.com/eks/latest/userguide/install-kubectl.html

Helm 安装:https://docs.thinkwithwp.com/eks/latest/userguide/helm.html

运行以下命令创建 EKS 集群,本实验环境中的集群版本为 V1.21

eksctl create cluster --name test-cluster

创建完成后,检查集群连接性,应该会看到两个 Ready 状态的节点

kubectl get node

安装EBS CSI Driver 插件,将 arn 中的 111122223333 替换为您的账户 ID;

eksctl utils associate-iam-oidc-provider \
  --cluster test-cluster \
  --approve
eksctl create iamserviceaccount \
  --name ebs-csi-Controller-sa \
  --namespace kube-system \
  --cluster test-cluster \
  --attach-policy-arn arn:AWS:iam::AWS:policy/service-role/AmazonEBSCSIDriverPolicy \
  --approve \
  --role-only \
  --role-name AmazonEKS_EBS_CSI_DriverRole
eksctl create addon --name AWS-ebs-csi-driver --cluster test-cluster --service-account-role-arn arn:AWS:iam::111122223333:role/AmazonEKS_EBS_CSI_DriverRole --force

作为生产环境EBS卷加密前的模拟,接下来创建一个 StorageClass,和一个 Redis 的 StatefulSet:

curl -LJO https://raw.githubusercontent.com/henghengha/hello-world/master/sc.yml
curl -LJO https://raw.githubusercontent.com/henghengha/hello-world/master/sts.yml
curl -LJO https://raw.githubusercontent.com/henghengha/hello-world/master/rbac.yml
curl -LJO https://raw.githubusercontent.com/henghengha/hello-world/master/cm.yml
kubectl apply -f sc.yml
kubectl apply -f rbac.yml
kubectl apply -f cm.yml
kubectl apply -f sts.yml

检查部署状态

查看 EBS 卷均未加密

3.2 方案实现

EBS卷是否加密的属性是在 StorageClass 对象中声明的,通过重建 StorageClass 可以保证重新创建出来的 pv 即 EBS 卷是加密的,但是不会作用到之前已经存在的 EBS 卷。本部分将重点展示针对有状态对象在启用存储加密后,如何将原有 Pod 的数据备份,从而确保所有的 EBS 卷都已加密。操作步骤如下:

安装 Snapshot Controller,默认安装在 kube-system 命名空间,确保运行正常,参考:

https://thinkwithwp.com/blogs/containers/using-ebs-snapshots-for-persistent-storage-with-your-eks-cluster

注意:配置文件中Snapshot-Controller 使用的镜像地址为gcr.io/Kubernetes-staging-sig-storage/Snapshot-Controller:v5.0.1,如果在中国区部署,请将该镜像下载下来,可以上传至 ECR,然后修改配置文件中的地址为中国区ECR的地址

生产环境中为了尽可能的减少业务受影响的时间,我们后面将使用相同的配置创建一个新的 StatefulSet 对象,命名为 redis1,根据命名规则(该示例 StatefulSet 中的 volumeClaimTemplates 的名字为 data),redis1 生成的 pvc 名字将是 data-redis1-0 和 data-redis1-1。注意这里先不做真正的 StatefulSet 的部署;

删除原有 StorageClass,并使用如下配置重建:

apiVersion: storage.Kubernetes.io/v1
kind: StorageClass
metadata:
  name: ebs-sc
provisioner: ebs.csi.AWS.com
  parameters:
    encrypted: 'true'
volumeBindingMode: WaitForFirstConsumer

本示例使用托管的 AWS KMS key aws/ebs 来进行加密,因此在 StorageClass 的配置中未指定具体的 key ID。如果需要使用自定义 KMS 密钥,可以在 StorageClass 中添加 kmsKeyId 配置。与此同时,遵循最小权限原则,还需要给 EBS CSI Driver 授权 KMS 的对应权限。创建 IAM policy 并关联至AmazonEKS_EBS_CSI_DriverRole,策略内容可参考官方文档进行配置:

https://docs.thinkwithwp.com/eks/latest/userguide/csi-iam-role.html

为原有的未加密 EBS 卷创建快照,并进行快照恢复到加密卷。首先,为 pvc data-redis-0 和data-redis-1 创建 Snapshot:

a.创建 Volume Snapshot Class;

cat <<"EOF" > Snapshot-class.yaml
apiVersion: Snapshot.storage.Kubernetes.io/v1
kind: VolumeSnapshotClass
metadata:
  name: test-snapclass
driver: ebs.csi.AWS.com
deletionPolicy: Delete
EOF
kubectl apply -f Snapshot-class.yaml

b.创建 YAML 文件,volume-Snapshot-redis.yaml 内容如下。注意:VolumeSnapshot 中的 persistentVolumeClaimName 要保证与 Pod 对应的 pvc 名称相同,此处分别为 data-redis-0 和data-redis-1. 部署该文件来创建快照;

cat <<"EOF" > volume-Snapshot-redis.yaml
apiVersion: Snapshot.storage.Kubernetes.io/v1
kind: VolumeSnapshot
metadata:
  name: redis-0-Snapshot
spec:
  volumeSnapshotClassName: test-snapclass
  source:
    persistentVolumeClaimName: data-redis-0
---
apiVersion: Snapshot.storage.Kubernetes.io/v1
kind: VolumeSnapshot
metadata:
  name: redis-1-Snapshot
spec:
  volumeSnapshotClassName: test-snapclass
  source:
    persistentVolumeClaimName: data-redis-1
EOF

kubectl apply -f volume-Snapshot-redis.yaml

c.检查快照信息

使用快照作为数据源创建 pvc,给下一步新创建的 StatefulSet 中的 Pod 使用:

cat <<"EOF" > volume-from-Snapshot.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-redis1-0
spec:
  storageClassName: ebs-sc
  dataSource:
    name: redis-0-Snapshot
    kind: VolumeSnapshot
    apiGroup: Snapshot.storage.Kubernetes.io
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 4Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-redis1-1
spec:
  storageClassName: ebs-sc
  dataSource:
    name: redis-1-Snapshot
    kind: VolumeSnapshot
    apiGroup: Snapshot.storage.Kubernetes.io
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 4Gi
EOF

kubectl apply -f volume-from-Snapshot.yaml

部署新的 StatefulSet redis1,并检查状态:

确认 pv 对应的 EBS 卷已加密,并验证数据备份无误之后可以根据具体的配置情况将连接信息切到新的对象上。如果确认所有流量都已转移到新的Pod上,即可删除原有的StatefulSet资源。

3.3 环境清除

删除集群中部署的资源,并删除pvc 和 pv 对象:

kubectl delete -f sts.yml
kubectl delete -f rbac.yml
kubectl delete -f cm.yml

删除集群:

eksctl delete iamserviceaccount --cluster test-cluster --name ebs-csi-Controller-sa
eksctl delete nodegroup {your-node-group-name} --cluster test-cluster
eksctl delete cluster --name test-cluster

4.小结

在这个博客里我们针对EKS上有状态服务的数据保护安全需求,介绍了结合KMS服务,在EKS中如何使用StatefulSet、Snapshot Controller 的方式实现有状态服务的存储加密启用,该方式通过从底层存储解决数据加密存储及迁移,无需应用层介入数据迁移,从而简单有效解决了EKS上有状态服务的存储加密需求。

5.参考

https://docs.thinkwithwp.com/zh_cn/eks/latest/userguide/ebs-csi.html

https://thinkwithwp.com/blogs/containers/using-ebs-snapshots-for-persistent-storage-with-your-eks-cluster/

https://eksctl.io/

本篇作者

李锐

亚马逊云科技金融行业资深解决方案架构师,负责基于亚马逊的云计算方案架构咨询和设计,擅长安全领域。加入 AWS 之前,曾任职互联网公司,拥有多年金融云产品及架构设计经验。

孙彦巧

亚马逊云科技金融行业解决方案架构师,负责云计算解决方案的架构设计和咨询。有多年 AWS 从业经验,热衷于容器、微服务架构及 Devops 方面的研究和应用。