亚马逊AWS官方博客

在 AWS 中国区 EKS 上以 GitOps 方式构建 CI/CD 流水线

2017年,Weaveworks的CEO Alexis在他的一篇名为《GitOps – Operations by Pull Request》的博客里第一次将GitOps这个概念正式带入到世人面前,开创性的让专注于应用的开发者们无需关注深层次的应用交付和随之而来的复杂运维,转而使用他们更为熟悉的Git命令,就能够轻松地在声明式基础架构(如Kubernetest)上交付和运维声明式应用负载。经过近三年的发展,GitOps这个理念已经被越来越多的用户所接受,围绕GitOps的一系列最佳实践,我们可以简化传统的CI/CD流水线,构建更为高效和简洁的GitOps方式的CI/CD流水线。

在这篇博客里,我们就来一起实践一下如何在AWS中国区的EKS环境里构建GitOps风格的CI/CD流水线。其中CI/CD流水线中的代码仓库我们使用GitHub,CI引擎我们采用最为常用的开源持续集成工具Jenkins,CD引擎使用GitOps理念的始作俑者WeavWorks的Flux(您也可以根据实际需求灵活使用和组合不同的CI/CD生态链工具构建您的GitOps CI/CD流水线,GitOps本身对工具并没有严格要求)。我们会通过演示一个简单的Flask Web应用如何在以GitOps方式构建的CI/CD流水线上实现应用的持续集成和持续交付;同时我们还会演示将这个应用改造成Helm Chart,以GitOps方式管理和发布Helm应用。

1         什么是GitOps

简单来说,GitOps是实现持续交付的一种方式。通过使用Git作为唯一的事实来源,去声明基础架构以及应用负载所期望达到的最终运行状态。GitOps持续交付工具会实时观察声明式基础架构本身以及运行其上的工作负载状态,并与保存在Git中所期望的配置和部署文件比较差异。如果有差异,则会自动触发一系列预先配置好的自动更新或回滚策略,以确保基础架构和工作负载始终按照Git中的配置文件及部署文件所描述的期望状态运行。

对于本次GitOps CI/CD实践下的EKS和运行其上的工作负载来说,Kubernetest集群和工作负载的配置修改和状态变更是源自于Git中代码的变化(通过git push或者pull request触发并完成最终交付,GitOps推荐使用pull request),而不再是传统的CI/CD流水线中由CI引擎所发起的使用kubectl create/apply或helm install/upgrade直接操作集群实现最终交付。

关于详细的GitOps介绍,可以参考《GitOps – What you need to know》

2         GitOps CI/CD实践

本次实践所构建的一个最为基础的GitOps CI/CD流水线如下图所示:

 

实验环境在AWS的部署架构如下图所示:

 

其中代码仓库GitHub包含两个代码库,一个为保存应用代码的app-repo,另一个为保存应用配置和部署文件的config-repo。CI/CI流水线中的持续集成引擎Jenkins和持续交付引擎Flux全部以Pod方式部署在Amazon EKS环境中。

基本的工作流程如下:

  • 开发者在开发环境中编写代码,并将最终完成的应用代码推送至app-repo;
  • 应用代码库app-repo中的代码变更触发Jenkins CI引擎,Jenkins按照既定的Pipeline对代码进行编译打包并生成容器镜像,然后将其推送至Amazon ECR容器镜像仓库;
  • 部署在EKS环境中的CD引擎Flux周期性扫描ECR容器镜像仓库并从中拉取应用的容器镜像元数据,当发现有新版本的容器镜像产生时会自动将新版本的容器镜像地址通过git commit/push同步至保存在config-repo中的应用部署文件;
  • Flux周期性拉取config-repo代码库中的应用配置和部署文件,并比较集群当前的应用负载运行状态是否和config-repo中的文件所描述的期望一致,当发现二者有差异时,Flux会自动将差异同步至EKS集群,确保工作负载始终按照期望状态运行。

以上是一个相对简单的GitOps CI/CD流水线示例,包含了GitOps的最基本也是最为核心的关键要素,在实际的生成环境中可以根据需求基于此流水线做进一步延伸和扩展,比如引入Git的分支管理,针对不同分支构建不同的GitOps CI/CD流水线;引入相应的单元测试、功能测试、压力测试等多个测试环节;引入安全基线保障,如代码扫描、镜像扫描、漏洞扫描等。

此外,示例中为了演示方便,对于代码的提交和整个CI/CD流程的触发我们使用了基于git push的机制。在实际的生产环境中,GitOps更为推荐使用的是基于pull request方式去更新主干分支的代码仓库,这样可以在触发生产环境的CI/CD流水线前,通过GitHub引入必要的人工代码审核和批复流程,而无需在CI/CD环节插入不必要的人工干预降低效率,这也更符合多人协作的Git实际使用场景,同时也满足企业客户的合规需求。生产环境中典型的GitOps CI/CD流水线构建可参考下图:

 

2.1   在中国区(如北京cn-north-1)创建EKS集群

EKS集群创建请参考Amazon EKS用户指南中的Amazon EKS入门,本次示例我们采用eksctl安装方式,示例命令如下(请确保执行eksctl命令前,系统已安装最新的AWS CLI,并配置好相关的CLI凭证AKSK;安装最新版本的eksctl工具;安装1.15版本的kubectl客户端):

eksctl create cluster \
--name gitops-cicd \
--region cn-north-1 \
--nodegroup-name gitops-workers \
--node-type t3.medium \
--nodes 2 \
--nodes-min 1 \
--nodes-max 2 \
--ssh-access \
--ssh-public-key <your public key> \
--managed

* 注:< your public key >需要改成您自己的key,您也可以根据实际需求自行定义创建相应集群

集群创建完成后,查看集群服务信息:

kubectl get svc

参考输出:

 

查看工作节点状态:

kubectl get nodes

参考输出:

 

2.2   为EKS部署ALB Ingress Controller

在本次示例中,我们使用更为主流的AWS Application Load balancer去部署应用,所以需要预先在EKS上部署并配置好ALB Ingress Controller。在AWS中国区EKS上部署及配置ALB Ingress Controller的详细方法请参考eks-workshop-greater-china中的4.2 使用ALB Ingress Controller

按照文档所示部署好ALB Ingress Controller后,可用如下命令确认是否成功运行:

kubectl get pods -n kube-system

参考输出:

 

2.3   在本地系统部署Helm客户端程序

在本次示例中,GitOps CI/CD流水线中的CI引擎Jenkins以及CD引擎Flux我们都采用Helm chart方式进行部署,因此需要预先在本地系统安装好Helm客户端程序,安装命令如下:

curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 > get_helm.sh
chmod 700 get_helm.sh
./get_helm.sh

安装完成后验证是否安装成功

Helm version

参考输出:

 

2.4   通过Helm安装Jenkins

2.4.1        Jenkins安装

首先,为Helm添加安装Jenkins的chart源,考虑道国内网络连接问题,在本次示例里我们为Helm添加国内chart源,命令如下:

helm repo add stable https://burdenbear.github.io/kube-charts-mirror/

参考输出:

 

查询Jenkins chart包并下载,解压后编辑其中的values.yaml文件参数

cd ~/
helm search repo jenkins
helm pull stable/jenkins
tar -zvxf jenkins-1.18.0.tgz
cd jenkins
vim values.yaml

取消adminPassword注释,添加自定义的Jenkins登陆密码,如下所示:

# you should revert master.adminUser to your preferred admin user:
adminUser: "admin"
adminPassword: "Passw0rd"

修改servicePort为80端口,如下所示:

servicePort: 80
targetPort: 8080
# For minikube, set this to NodePort, elsewhere use LoadBalancer
# Use ClusterIP if your setup includes ingress controller
serviceType: ClusterIP

修改ingress配置,enabled设置为true,apiVersion使用新版本,添加对应的alb annotations信息,如下所示:

  ingress:
    enabled: true
    # For Kubernetes v1.14+, use 'networking.k8s.io/v1beta1'
    apiVersion: "networking.k8s.io/v1beta1"
    labels: {}
    annotations:
      kubernetes.io/ingress.class: alb
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip

 

其他默认参数保持不变,也可以根据实际环境需要做相应调整,比如调整Jenkins Master/Slave的CPU/Mem份额,PVC里的存储空间大小等。通过Helm chart方式可以更加便捷快速的部署Jenkins,同时也给予用户灵活的参数调整空间。

创建jenkins在EKS里安装的命名空间,执行如下命令:

kubectl create ns gitops-jenkins

安装Jenkins,执行如下命令:

helm install gitops-jenkins -f values.yaml stable/jenkins -n gitops-jenkins

参考输出:


可以通过下述命令实时观察Jenkins部署状态,当deployment状态为ready后就可以访问Jenkins进行后续的配置。Jenkins部署过程中会从国外站点拉取镜像及一些配置文件,因为国内网络访问国外站点问题,Jenkins的部署过程可能会持续较长时间。

kubectl get deployment -n gitops-jenkins -w

 

2.4.2        Jenkins Slave节点镜像准备

在等待Jenkinsan安装部署过程中我们可以同时准备一下Jenkins Slave所需要的docker image,本次示例中CI引擎Jenkins Master节点我们会以Pod的形式部署在EKS中,同时Jenkins Master会根据CI Job的需求动态启动Jenkins Slave Pod完成代码的编译和打包工作,所以需要预先准备好Jenkins Slave节点所需的容器镜像,本次示例中我们所演示的是一个基于Python的Flask Web应用,所以Jenkins Slave节点只需要具备基本的docker命令执行环境即可完成Flask Web App的容器镜像打包和上传镜像仓库,同时我们还会将kubectl客户端程序也打包在这个镜像中,这样就可以通过Jenkins Slave节点直接操作EKS集群,部署传统的CI/CD流水线(注:该流水线中Jenkins同时作为CI引擎和CD引擎)。

创建Jenkins Slave节点容器镜像的Dockerfile

cd ~/
mkdir dockerfile
cd dockerfile
cat << EOF > Dockerfile
FROM jenkins/jnlp-slave
MAINTAINER DaoMing
USER root
RUN wget https://download.docker.com/linux/static/stable/x86_64/docker-18.06.3-ce.tgz \
&& tar xzvf docker-18.06.3-ce.tgz \
&& cp docker/* /usr/bin/
RUN curl -o kubectl https://amazon-eks.s3.cn-north-1.amazonaws.com.cn/1.15.10/2020-02-22/bin/linux/amd64/kubectl \
&& chmod +x ./kubectl \
&& mv ./kubectl /usr/local/bin/kubectl
EOF

 

登陆AWS中国区Console选择ECR服务,创建Jenkins Slave节点的容器镜像仓库”jenkins-slave”,如下图所示

 

构建jenkins-slave镜像并上传至该镜像仓库中(请在执行下述命令前确保您的本地环境已经安装好docker运行环境):

sudo aws ecr get-login-password --region cn-north-1 | docker login --username AWS --password-stdin <your account id>.dkr.ecr.cn-north-1.amazonaws.com.cn
sudo docker build -t jenkins-slave .
sudo docker tag jenkins-slave:latest <your account id>.dkr.ecr.cn-north-1.amazonaws.com.cn/jenkins-slave:latest
sudo docker push <your account id>.dkr.ecr.cn-north-1.amazonaws.com.cn/jenkins-slave:latest

镜像推送成功后记录好镜像地址,如

<your account id>.dkr.ecr.cn-north-1.amazonaws.com.cn/jenkins-slave:latest

* 注:<your account id>需要修改成您自己的account id

 

2.5   Jenkins配置及安装相关插件

2.5.1         Jenkins 权限配置

当Jenkins Deployment和Pod都已经成功运行后,在访问Jenkins界面做后续配置前,我们还需要重新设置一下Jenkins在EKS里的角色和权限(RBAC),使其具备跨命名空间的ClusterRole权限,命令如下:

cd ~/
mkdir rbac
cd rbac
cat << EOF > clusterrole.yaml
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: gitops-jenkins
rules:
  - apiGroups: ["extensions", "apps"]
    resources: ["deployments"]
    verbs: ["create","delete","get","list","watch","patch","update"]
  - apiGroups: [""]
    resources: ["services"]
    verbs: ["create","delete","get","list","watch","patch","update"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["create","delete","get","list","patch","update","watch"]
  - apiGroups: [""]
    resources: ["pods/exec"]
    verbs: ["create","delete","get","list","patch","update","watch"]
  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get","list","watch"]
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get"]
EOF
cat << EOF > clusterrolebinding.yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: gitops-jenkins
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: gitops-jenkins
subjects:
  - kind: ServiceAccount
    name: gitops-jenkins
    namespace: gitops-jenkins
EOF
kubectl apply -f .

 

2.5.2         Jenkins 插件安装及配置

通过界面访问Jenkins并进行后续的配置,使用下述命令查看Jenkins访问地址:

kubectl get ingress -n gitops-jenkins

 

参考输出:

通过Helm chart方式安装好的Jenkins默认会安装好kubernetes插件,我们还需要安装本次示例所需要的GitHub插件以及AWS ECR容器镜像仓库插件,使用我们之前设置好的Jenkins用户名和密码登陆Jenkins图形界面进行相关插件的安装和升级。安装插件前建议将Jenkins插件包安装源更换为国内源,这样下载速度会更快。

 

插件全部安装好后我们首先来配置kubernetes插件,”Manage Jenkins” -> “Manage Nodes and Clouds“ -> ”Configure Clouds“。默认情况下,因为Jenkins Master节点是安装在EKS之上的一个Pod,所以无需再为其配置相应的认证信息即可和EKS(kubernetes)正常通讯,我们点击页面上的”Kubernetes Cloud details” -> ”Test Connection” 验证一下Jenkins和EKS控制平面是否连接是否正常,如下图所示:

 

点击页面上的”Pod Templates” -> “Pod Template details”,将”Usage”更改为”Only build jobs with label expressions matching this node”;”Docker image”更改为之前Jenkins Slave节点镜像推送到的ECR镜像地址;”Volumes” -> “Add Volume” -> “Host Path Volume”添加Host path: /var/run/docker.sock, Mount path: /var/run/docker.sock,这主要是为Jenkins Slave Pod能和宿主机的Docker守护进程通信,利用宿主机Docker daemon执行Docker命令 ;”Service Account”填入Jenkins Pod所属的服务账户”gitops-jenkins”;其他配置采用默认参数即可。上述配置如下面图例所示:

 

 

至此,我们就完成了CI引擎Jenkins安装和基本配置。

2.6   构建CI流水线

接下来我们构建GitOps CI/CD流水线中的CI持续集成部分。

2.6.1       ­ 示例应用代码、GitHub应用代码仓库以及ECR容器镜像仓库准备

在GitHub上创建CI流水线中我们所需保存应用代码的代码仓库,如” app-repo” ,并clone至本机(请确保执行下述操作前,本机已安装好相应的git客户端程序)。

 

cd ~/
git clone https://github.com/<your github>/app-repo

下载本次示例所使用的示例代码到当前目录,解压并将解压后的app目录中的文件移动至git clone的app-repo目录中,git push回GitHub代码仓库。

unzip gitops.zip
mv gitops/app/* app-repo/
cd app-repo
git add .
git commit -m "the first commit"
git push

执行成功后,GitHub app-repo应如下图所示:

 

app-repo代码库中的应用代码的详细结构如下:

app-repo
|-- app.py
|-- Dockerfile
|-- Jenkinsfile
|-- README.md
|-- requirements.txt
|-- static
|   `-- css
|       `-- mystyle.css
`-- templates
    `-- index.html

 

登陆AWS中国区Console选择ECR服务,创建示例应用的容器镜像仓库” gitops-app-demo”,如下图所示:

 

2.6.2        配置Jenkins CI流水线

我们首先为Jenkins执行CI作业创建一些必须的访问密钥,包括Jenkins访问GitHub的密钥,以及Jenkins访问容器镜像仓库ECR的密钥。

配置GitHub访问密钥:

Jenkins主页面,点击” Credentials” -> “System” -> “Global credentials(unrestricted)” -> “Add Credentials”,”Kind”选择”Username and password”,依次输入GitHub的用户名、密码及其他信息,如下图所示:

 

配置ECR访问密钥:

Jenkins主页面,点击” Credentials” -> “System” -> “Global credentials(unrestricted)” -> “Add Credentials”,”Kind”选择”AWS Credentials”,填入能够读写ECR容器镜像仓库的角色的AKSK(稍后在配置Pipeline脚本中的ECR访问权限时会通过这个录入的AKSK去访问对应的ECR镜像仓库),如下图所示:


配置Jenkins Pipeline:

认证信息准备完毕后我们接下来创建相应的CI流水线,回到Jenkins主页面,点击“New Item“ 创建”Pipeline”,输入项目名称,如: gitops-cicd,创建流水线。

 

“gitops-cicd”流水线中”Build Triggers”,选择” GitHub hook trigger for GITScm polling”,该选项意味着在本次示例中GitHub代码仓库中相应的状态变化会使用GitHub Webhook方式触发Jenkins CI流水线完成后续的持续集成工作。“gitops-cicd”流水线中”Pipeline” -> “Defination” 选择 ”Pipeline script from SCM”,该选项意味着Jenkins用于持续集成的流水线文件也是同样保存在代码仓库中,可以由提交CI作业的人员自行修改,赋予更大的灵活性(本次示例所需要的流水线文件Jenkinsfile我们已经预先保存在之前下载的应用代码的根目录下,流水线以声明式语法编写,可直接编辑使用)。”SCM”选择”Git”,”Repository URL” 填入GitHub中app-repo的地址(https://github.com/<your github>/app-repo),”Credential”选择之前预先配置好的GitHub认证信息,其他配置使用默认参数,也可以根据实际需求做灵活调整。上述配置如下面图例所示:

 

 

配置GitHub连接Jenkins 的Webhook:

访问https://github.com/<your github>/app-repo/settings/hooks,点击”add webhook”, “Payload URL”中填入<your jenkins server URL>/github-webhook/( 例如:http://a90b4cb5-gitopsjenkins-git-9a34-1517645282.cn-north-1.elb.amazonaws.com.cn/github-webhook/ ),其他采用默认参数,如下图所示:

 

       编辑app-repo下的Jenkinsfile文件,构建可执行的Pipeline

       切换到本机命令行工具界面,执行下述命令:

cd ~/
cd app-repo
vim Jenkinsfile       

Jenkinsfile样例模版代码如下,其中标红部分需要替换成您当前的配置参数:

pipeline {
    agent {
        label 'gitops-jenkins-jenkins-slave'
    }
    stages {
        stage('Get Source') {
            steps {
                echo "1.Clone Repo Stage"
                git credentialsId: 'github', url: 'https://github.com/<your github>/gitops-app'
                script {
                    build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
                    repo_name = '<your account id>.dkr.ecr.cn-north-1.amazonaws.com.cn'
                    app_name = 'gitops-app-demo'
                }
            }
        }
        stage('Build Image') {
            steps {
                echo "2.Build Docker Image Stage"
                sh "docker build --network host -t ${repo_name}/${app_name}:latest ."
                sh "docker tag ${repo_name}/${app_name}:latest ${repo_name}/${app_name}:${build_tag}"
            }

        }
        stage('Push Image') {
            steps {
                echo "3.Push Docker Image Stage"
                withDockerRegistry(credentialsId: 'ecr:cn-north-1:ecr-repo', url: 'https://<your account id>.dkr.ecr.cn-north-1.amazonaws.com.cn/gitops-app') {
                    sh "docker push ${repo_name}/${app_name}:latest"
                    sh "docker push ${repo_name}/${app_name}:${build_tag}"
                }
            }
        }
    }
}

 

其中,git credentialsId: ‘github’, url: ‘https://github.com/<your github>/gitops-app’ 这段代码主要是Jenkins流水线调用GitHub访问密钥,这段代码可以通过Jenkins ”Pipeline Syntax”功能快速生成。进入Jenkins主界面,选择之前创建好的”gitops-cicd” Pipeline,点击” Pipeline Syntax”,”Sample Step”选择”git:Git”,”Repository URL”填写本次示例应用的代码仓库地址,如:https://github.com/<your github>/app-repo,”Credentials” 选择之前配置好的GitHub访问密钥,其他采用默认参数,点击”Generate Pipeline Script”生成代码,替换Jenkinsfile中GitHub访问密钥代码,如下图所示:

 

其中,repo_name = ‘<your account id>.dkr.ecr.cn-north-1.amazonaws.com.cn’代码中<your account id>部分替换为您创建的应用镜像代码仓库所属的账户ID。

其中,withDockerRegistry(credentialsId: ‘ecr:cn-north-1:ecr-repo’, url: ‘https://<your account id>.dkr.ecr.cn-north-1.amazonaws.com.cn/gitops-app’)这段代码主要是Jenkins流水线调用镜像仓库ECR的访问密钥,这段代码也可以通过Jenkins ”Pipeline Syntax”功能快速生成。点击” Pipeline Syntax”,”Sample Step”选择”withDockerRegistry: Sets up Docker registry endpoint”,”Docker registry URL”填写” https://<your account id>.dkr.ecr.cn-north-1.amazonaws.com.cn/gitops-app-demo”,”Registry credentials”选择AWS AKSK所关联的对应Region的ECR,如cn-north-1,点击”Generate Pipeline Script”生成代码,替换Jenkinsfile中ECR访问密钥代码,如下图所示:

 

保存修改后的Jenkins文件,执行下述命令:

git add .
git commit -m "modified Jenkinsflie"
git push

至此,我们已经完成CI流水线的全部构建工作。返回至Jenkins主页面,选择”gitops-cicd”流水线,点击”Build Now”,当CI Job开始运行后查看”Console Output”中的日志记录,观察CI流水线的每一步执行情况,当CI流水线全部执行完毕且日志显示成功后,“gitops-cicd”会出现蓝色的success的状态,同时通过AWS Console查看ECR服务中的示例应用的容器镜像仓库,可以看到已经有新的容器镜像被Jenkins推送过来,参考输出如下列图例所示:

 

2.7   构建CD流水线

接下来我们构建GitOps CI/CD流水线中的CD持续交付部分。

2.7.1         创建示例应用的GitHub配置代码仓库

在GitHub上创建CD流水线中用于保存应用部署和配置信息的代码仓库,如” config-repo” ,并clone至本机。

 

cd ~/
git clone https://github.com/<your github>/config-repo

将之前下载的示例代码解压后的config目录中的文件移动至git clone的config-repo目录中,修改应用部署配置文件中的镜像地址,推送回GitHub代码仓库。

mv gitops/config/* config-repo/
cd config-repo/workloads
vim gitops-example-dep.yaml

gitops-example-dep.yaml模版文件如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gitops-example-deployment
  namespace: gitops-cicd
  labels:
    app: gitops-example
  annotations:
# Container Image Automated Updates
fluxcd.io/automated: "true"
spec:
  selector:
    matchLabels:
      app: gitops-example
  replicas: 3
  template:
    metadata:
      labels:
        app: gitops-example
    spec:
      containers:
      - name: gitops-example
        image: <your image URL>
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80

 

注:其中annotations部分的fluxcd.io/automated: true是设置Flux可以根据部署文件中更新后的image url自动更新应用镜像。关于Deployment文件针对Flux更多的annotations配置参数请参考官方说明文档

将yaml文件中的” image: <your image URL>”更改为CI流水线中由Jenkins推送至ECR中的新的应用镜像地址,如”image: <your account id>.dkr.ecr.cn-north-1.amazonaws.com.cn/gitops-app-demo:<tag>“,请注意tag不要使用latest。保存后执行下述命令:

cd ..
git add .
git commit -m "the first commit"
git push
 执行成功后,GitHub config-repo应如下图所示:

 

config-repo代码库中的应用代码的详细结构如下:

config-repo/
|-- namespaces
|   `-- gitops-cicd.yaml
|-- README.md
`-- workloads
    |-- gitops-example-dep.yaml
    |-- gitops-example-ingress.yaml
    `-- gitops-example-svc.yaml

2.7.2         通过Helm安装Flux

Flux是GitOps中最为关键的持续交付引擎,在整个CI/CD流水线中类似于一个观察员和操作员,不断的观察当前集群和应用负载状态是否和git中所保存的期望配置及部署一致,如果不一致则介入做相应的同步调整。Flux原理架构参见下图,详细介绍请参考Flux官方文档

Flux的安装很简单,首先为Helm添加Flux的chart源,创建Flux所属命名空间,并安装Flux,执行以下命令:

cd ~/
helm repo add fluxcd https://charts.fluxcd.io
kubectl create ns gitops-flux
helm upgrade -i flux fluxcd/flux \
--set git.url=git@github.com:<your github>/config-repo \
--namespace gitops-flux
kubectl get pods -n gitops-flux -w

将<your github>替换为您自己的GitHub,等待一些时候,待flux 和flux-memcached这两个pod处于running状态后就表示flux已经成功安装。

2.7.3         配置Flux CD流水线

Flux访问GitHub应用配置库配置:

为了能让Flux访问保存在GitHub里的应用配置库,同时可以根据读取到的ECR中新推送的应用镜像版本动态修改应用配置库中的镜像配置文件,实现应用的自动化部署,我们需要为Flux赋予可读写GitHub上示例应用的配置代码库的权限。可以通过将Flux在创建过程中默认生成的SSH Key中的Public Key作为GitHub应用配置库的Deploy key来授予相应权限。Public key可以通过Flux的客户端工具fluxctl查看,也可以直接通过查询Flux Pod日志获取。

本次示例我们将使用fluxctl来获取public key,为此需要先安装fluxctl。

sudo wget -O /usr/local/bin/fluxctl https://github.com/fluxcd/flux/releases/download/1.19.0/fluxctl_linux_amd64
chmod +x /usr/local/bin/fluxctl
fluxctl identity --k8s-fwd-ns gitops-flux

将ssh-rsa开头的public key拷贝下来,作为GitHub应用配置库的Deploy key。访问https://github.com/<your github>/config-repo/settings/keys,点击”Add deploy key”,输入”Title”,”Key”,选择”Allow write access”如下图所示:


至此,Flux应该已经能访问config-repo中的应用配置文件,并具备读写repo权限,我们可以通过如下命令,查看flux pod日志,确认对应用配置repo的权限是否正常。

kubectl logs flux-86857bb8b7-v8ngf -n gitops-flux -f --tail=20

其中”flux-86857bb8b7-v8ngf”请替换为您当前flux pod的名字(使用”kubectl get pods -n gitops-flux”命令查看)

当日志中有类似如下输出时,表示Flux已经正常访问Github上的应用配置库并具备相应的读写权限。

 

执行以下命令

kubectl get ns
kubectl get all -n gitops-cicd
kubectl get ingress -n gitops-cicd

参考输出:

可以看到Flux已经按照GitHub中的应用配置库里的配置文件部署好相应的应用程序,我们可以通过访问Ingress地址直接访问应用程序,参考输出如下:

Flux访问示例应用的ECR镜像仓库配置:

观察flux pod日志,此时应该会看到大量日志报没有权限访问” 918309763551.dkr.ecr.cn-north-1.amazonaws.com.cn/xxxx”以及” <your account id>.dkr.ecr.cn-north-1.amazonaws.com.cn/gitops-app-demo”,这是因为Flux会根据EKS上当前运行的workload自动扫描与其相关的所有使用到的镜像文件在镜像仓库中的元数据,其中就包括EKS系统镜像(如帐号为”918309763551” 的ECR所关联的所有EKS系统镜像)以及真正的应用镜像(如帐号为您保存应用镜像的gitops-app-demo镜像仓库)。前一类报错我们需要规避Flux去主动扫描一些不该扫描的镜像仓库,可以通过修改flux deployment文件中的”–registry-exclude-image”参数限定不要扫描EKS的ECR 系统镜像仓库;后一类报错主要是因为当前flux版本(1.19.0)还存在一个对AWS中国区ECR支持的bug,目前该bug已经被识别并被修复,相信在下一个flux版本release时就会得以彻底解决。

针对于上述两类报错我们做如下操作:

解决flux扫描系统镜像问题:

kubectl edit deployment flux -n gitops-flux

在deployment文件中的spec -> template -> spec -> containers -> args部分添加- –registry-exclude-image=918309763551.dkr.ecr.cn-north-1.amazonaws.com.cn/*,如下图所示:

修改完保存,待新的flux pod启动后观察日志,会发现flux不在扫描EKS系统镜像。

解决flux无法访问应用示例镜像仓库问题(详细的变通方案请参考此链接):

cd ~/
mkdir ecr-access
cat << EOF > ./ecr-access/rbac.yaml
---
kind: ServiceAccount
apiVersion: v1
metadata:
  name: ecr-k8s-secret-creator
  namespace: gitops-flux
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: ecr-k8s-secret-creator
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: ecr-k8s-secret-creator
roleRef:
  kind: ClusterRole
  name: ecr-k8s-secret-creator
  apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
  name: ecr-k8s-secret-creator
  namespace: gitops-flux
EOF
cat << EOF > ./ecr-access/deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: ecr-k8s-secret-creator
  namespace: gitops-flux
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ecr-k8s-secret-creator
  template:
    metadata:
      labels:
        app: ecr-k8s-secret-creator
    spec:
      serviceAccount: ecr-k8s-secret-creator
      containers:
        - image: bzon/ecr-k8s-secret-creator:latest
          name: ecr-k8s-secret-creator
          args:
            - "-secretName=ecr-docker-secret"
            - "-region=cn-north-1"
          resources:
            requests:
              cpu: 5m
              memory: 16Mi
            limits:
              cpu: 20m
              memory: 32Mi
EOF
kubectl apply -f ./ecr-access/
kubectl edit deployment flux -n gitops-flux

在deployment文件中的spec -> template -> spec -> containers -> args部分添加:

- --docker-config=/etc/fluxd/docker/config.json

如下图所示:

 

在deployment文件中的spec -> template -> spec -> containers -> volumeMounts部分添加:

- mountPath: /etc/fluxd/docker
          name: docker-config

在deployment文件中的spec -> template -> spec -> containers -> volumes部分添加:

- name: docker-config
        secret:
          defaultMode: 420
          secretName: ecr-docker-secret

如下图所示:

 

修改完保存,待新的flux pod启动后观察日志,会发现flux已经可以正常访问示例镜像的ECR镜像仓库。至此,我们就完成了CD流水线的构建工作。

如果想要调整Flux扫描ECR容器镜像库以及查询GitHub配置代码库的频度,可以通过修改flux deployment文件中的”–automation-interval”和”–git-poll-interval”参数,这两个参数默认时间为5分钟,可以更改为更小的时间间隔,如下图所示:

 

 

2.8  验证GitOps CI/CD流水线

cd ~/app-repo/
vim templates/index.html

修改应用版本为v2.0,并保存,如下图所示:

 

git add .
git commit -m "change to v2.0"
git push

切换至Jenkins界面,可以看到CI Job已经开始正常运行,如下图所示:

 

等待Jenkins CI Job执行完毕后,切换至本地CLI环境,观察flux pod日志,如下图所示:

 

从上述日志信息可以看到,Flux会定期扫描应用示例所在的ECR镜像仓库,当Jenkins CI Job执行完毕推送新的镜像至ECR镜像仓库后, Flux扫描到有新版本的镜像产生,会获取新镜像的URL并自动更新应用程序所在GitHub上的配置代码库中的应用部署文件(更新Deployment文件中的镜像URL);同时,Flux也会定期扫描应用示例所在的GitHub应用配置代码库,当发现应用配置文件有新的变更后,会自动将更新后的配置同步到集群,以确保应用始终按照GitHub应用配置库中的配置文件所描述的期望状态运行。可以看到GitOps CI/CD流水线执行完毕后,应用完成新版本的自动部署,如下图所示:

 

此外,得益于Git固有的版本管理功能,我们还可以直接使用Git命令快速完成应用版本的回滚,以实现更为便捷的应用恢复。参考执行以下命令,观察flux pod日志以及应用界面的版本更换情况:

cd ~/config-repo/
git pull
git log
git revert HEAD
git log
git push

至此,我们就完成了一个针对于简单的Flask Web应用的GitOps CI/CD流水线的构建和验证工作,开发人员通过更为熟悉的git命令就可以实现对应用的快速修改、部署以及回滚等操作;同时,从某种程度上来说,EKS(Kubernetes)集群本身也近乎于一个完整的声明式基础架构,基本上集群所包含的各个系统组件,集群之上所部署的相关插件模块以及运行其上的工作负载,我们都可以通过yaml文件以声明式方式进行描述(如,通过”kubectl get <object> -n <namespace> -o yaml”获取),并以GitOps方式进行管理。

2.9   以GitOps方式管理Helm发布

我们除了可以使用Flux以GitOps方式管理常规的以yaml配置文件发布应用的持续交付以及协同CI引擎一起构建CI/CD流水线外,还可以通过Helm Operator以GitOps方式管理以Helm chart方式发布的应用,实现应用的持续交付;同时Helm Operator还可以与Flux结合在一起使用,Flux会定期扫描容器镜像库中的新版本镜像,自动更新Helm Operator所管理的HelmRelease中的容器镜像,通过与Flux集成,我们可以打通CI和CD整个流水线,以GitOps方式实现更加自动化的Helm发布和应用的持续交付,如下图所示:

 

2.9.1        Helm Operator安装

切换至本地命令行界面,执行以下命令:

cd ~/
kubectl apply -f https://raw.githubusercontent.com/fluxcd/helm-operator/master/deploy/crds.yaml
helm upgrade -i helm-operator fluxcd/helm-operator \
--set git.ssh.secretName=flux-git-deploy \
--namespace gitops-flux \
--set helm.versions=v3
kubectl get pods -n gitops-flux -w

等待一些时候,待helm-operator这个pod处于running状态后就表示Helm Operator已经成功安装。

2.9.2        Helm 应用的配置代码准备

创建Helm应用部署的命名空间配置文件

cd ~/config-repo
cat << EOF > ./namespaces/gitops-helm.yaml
apiVersion: v1
kind: Namespace
metadata:
  labels:
    name: gitops-example
  name: gitops-helm
EOF

将示例应用的配置文件更改为Helm Chart发布方式,详细的Helm Chart构建请参考该链接

mkdir -p charts/gitops-helm-demo 
cat << EOF > ./charts/gitops-helm-demo/Chart.yaml
apiVersion: v2
name: gitops-helm-demo
version: 1.0.0
description: A Helm chart for GitOps Helm release demo
maintainers:
- name: Daoming
type: application 
EOF
cat << EOF > ./charts/gitops-helm-demo/values.yaml
# Default values for gitops-helm-demo.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 3
image:
  repository: <your account id>.dkr.ecr.cn-north-1.amazonaws.com.cn/gitops-app-demo
  tag: <your image tag>
EOF

*注:<your account id>部分,请替换为您相应的帐号信息,标红的<your image tag>部分,请替换为您当前ECR示例应用的容器镜像tag

mkdir -p charts/gitops-helm-demo/templates
cp workloads/gitops-example-dep.yaml charts/gitops-helm-demo/templates/deployment.yaml
cp workloads/gitops-example-ingress.yaml charts/gitops-helm-demo/templates/ingress.yaml
cp workloads/gitops-example-svc.yaml charts/gitops-helm-demo/templates/service.yaml

修改deployment.yaml文件,将namespace更改为gitops-helm,删除annotation描述,将replicas由3修改为{{ .Values.replicaCount }},将image地址变更为”{{ .Values.image.repository }}:{{ .Values.image.tag }}”

vim charts/gitops-helm-demo/templates/deployment.yaml

 

 

修改ingress.yaml文件,将namespace更改为gitops-helm。

vim charts/gitops-helm-demo/templates/ingress.yaml

 

 

修改ingress.yaml文件,将namespace更改为gitops-helm。

vim charts/gitops-helm-demo/templates/service.yaml

 

 

为Helm Operator创建HelmRelease配置文件,同时在该配置文件中按照固定格式详细定义其使用的镜像并添加针对于Flux的annotations配置参数,这使得当Flux读取到该镜像所在镜像库中有新版本镜像产生时则会同步更新HelmRelease配置文件中的镜像版本,从而使得Helm Operator可以根据新的HelmRelease配置文件同步更新EKS集群中所运行的Helm应用。关于Helm Operator和Flux的集成详细说明可以参考官方说明文档

mkdir releases
cat << EOF > ./releases/gitops-helm-demo.yaml
apiVersion: helm.fluxcd.io/v1
kind: HelmRelease
metadata:
  name: gitops-helm-demo
  namespace: gitops-helm
  annotations:
    fluxcd.io/automated: "true"
spec:
  releaseName: gitops-helm-demo
  chart:
    git: git@github.com:<your github>/config-repo
    ref: master
    path: charts/gitops-helm-demo
  values:
    image:
      repository: <your account id>.dkr.ecr.cn-north-1.amazonaws.com.cn/gitops-app-demo
      tag: <your image tag>
EOF

*注:请将代码中的<your github>,<your account id>,<your image tag>替换为您当前环境相应的信息

修改完后执行下述命令,将本地的Helm应用配置代码推送至GitHub:

git pull
git add .
git commit -m "add HelmRelease"
git push

通过下述命令验证Helm发布是否成功部署

kubectl get all -n gitops-helm
kubectl get helmreleases -n gitops-helm
kubectl get ingress -n gitops-helm

参考输出:

通过Ingress地址直接访问基于Helm方式部署的应用程序,参考输出如下:

2.9.3        验证GitOps方式下的Helm 应用的CI/CD流水线

cd ~/app-repo/
vim templates/index.html

修改应用版本为v3.0,并保存,如下图所示:

 

git add .
git commit -m "change to v3.0"
git push

查看Jenkins,确认CI Job是否正常运行,待Jenkins CI Job正常执行完后,查看Flux Pod日志以及Helm Operator Pod日志,可以看到当Flux获取到新版本镜像后会同步更新HelmRelease配置文件中的镜像tag,当HelmRelease配置文件中的镜像更新完成后, Helm Operator会自动将更新后的配置同步到集群,以确保应用始终按照GitHub应用配置库中的HelmRelease文件所描述的期望状态运行,最终实现新版本应用的部署,如下图所示:

 

3         小结

”What can be described can be automated” —— github:fluxcd/flux

自动化、高效率一直是IT行业永无止境的追求目标。在今天,声明式赋予了我们一种更为简洁更为直接的描述抽象事物的方式,从而衍生出类似于GitOps这种更为高效的持续交付的自动化手段。在这篇博客里,我们一起探讨并实践了使用GitHub+Jenkins+Flux+HelmOperator构建的GitOps CI/CD如何在声明式基础架构Amazon EKS上去构建和交付声明式应用。如我们在文章中所说,GitOps是一种持续交付的方式,包含了一系列的最佳实践,在构建CI/CD的工具层面并没有严格限制,只要符合GitOps的一些基本原则(Principles of GitOps)即可,希望大家从这篇博客里可以获得一些启发去构建自己的GitOps技术堆栈。

4         附录:参考资料

Amazon EKS 用户指南, https://docs.amazonaws.cn/eks/latest/userguide/what-is-eks.html

Amazon EKS Workshop, https://eksworkshop.com/

AWS GCR EKS Workshop, https://github.com/aws-samples/eks-workshop-greater-china

GitOps – What you need to know, https://www.weave.works/technologies/gitops/

Flux官方文档, https://docs.fluxcd.io/en/latest/introduction/

Flux GitHub链接, https://github.com/fluxcd/flux

Helm Operator官方文档, https://docs.fluxcd.io/projects/helm-operator/en/latest/

Helm Operator GitHub链接, https://github.com/fluxcd/helm-operator

ecr-k8s-secret-creator介绍, https://github.com/bzon/ecr-k8s-secret-creator

GitHub Pull Request说明, https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests

Jenkins Pipeline介绍, https://www.jenkins.io/doc/book/pipeline/

Helm Chart详细说明, https://helm.sh/docs/chart_template_guide/getting_started/

 

本篇作者

郭道明

AWS合作伙伴资深架构师,致力于合作伙伴技术生态构建和业务发展,喜欢尝试一切与众不同的新鲜事物。