GitOps是什么
GitOps 是 Weaveworks 提出的一种持续交付方式,它的核心思想是将应用系统的声明性基础架构 和应用程序存放在 Git 版本库中。将 Git 作为交付流水线的核心,每个开发人员都可以提交拉取请求 (Pull Request)并使用 Git 来加速和简化 Kubernetes 的应用程序部署和运维任务。通过使用像 Git 这样的简单工具,开发人员可以更高效地将注意力集中在创建新功能而不是运维相关任务上(例如,应用系统安装、配置、迁移等)。
GitOps主要包含的技术实践
1. Infrastructure as code
如k8s里应用部署的声明、pipeline as code等都应该属于基础设施的版本控制。
2. Pull request
所有的改动都应该通过合并请求review之后纳入主干分支,GitOps中会以git作为唯一可信源,去判 断应用当前的状态是否符合期望,同时也便于审计。
3. CI \ CD
主要涉及的软件包括Jenkins(X)、Gitlab CI 、Argo CD、Spinnaker、Flux等。
使用GitOps前后对比
在没有实践GitOps之前我们的部署过程如下图,我们称之为push模式。当我们需要部署的时候,通过工具或者人工的方式,将应用部署到k8s集群中。其中会带来一系列的问题,如需要依赖外部的客户端(kubectl等)才能操作,这会带来安全隐患,会把服务器以及k8s的访问权限暴露出来。同时操作也没办法进行审计和快速回滚,也没办法实时知道应用部署状态的反馈。
实践GitOps之后我们的部署过程如下图,我们称之为pull模式。可以看出整个过程是由部署在k8s内部的cd主动从git pull信息驱动的。它可以避免管理权限暴露带来的问题,同时所有的操作都有git做版本记录,cd平台会实时监控集群中应用的部署状态是否和git中期望的状态一致,能快速做出回滚等操作响应。
再贴一张整体的研发工作流,以便有一个更加宏观的认识
1. 研发提交代码到git发起合并请求,审查后合并到master,接下来触发持续集成,这里以Jenkins为例。
2. CI流水线大体包含的任务有,拉取代码 -> 构建镜像 -> 执行自动化测试 -> 归档制品(这里的制品主要是容器镜像)
3. CD部分,这里可以看到CD追踪了另一个代码库,也就是说一个git仓库是业务代码,一个仓库存放的是关于这个应用的描述,可以用helm、yaml或者是kustomize。由于部署操作是ArgoCD自动将git与集群中应用的状态进行对比。因此我们需要commit期望的应用状态(比如版本、副本数量等)到这个git仓库中,可以是由开发提交,也可以是由CI上的插件来辅助提交变更。ArgoCD检测到变更后,便会根据git中的定义,将应用部署或者是更新到集群中。
4. 其他的环节和本文主体关联不大,简单提一下还有准入门禁,这里使用了Gate Keeper,可以检测将要部署的应用有没有符合约定的规则,比如是否限制了memory。然后还有一块比较复杂的就是全链路的反馈机制,比如监控、日志、分布式追踪等等,以后有机会再谈。
GitOps实操
前面进行了基本的介绍,接下来就进行实操演示,这里主要是写Jenkins和Argo CD相关的操作,前置准备需要提前完成,包含了如下的东西。
- kubernetes集群,本文版本v1.21.4
- 使用calico作为网络组件、rook ceph作为组件、Traefik作为Ingress且完成域名相关配置、 minio作为对象存储、搭建好Gitlab。
1
使用helm在k8s里搭建Jenkins
直接使用Jenkins的helm工程部署
helm repo add jenkinsci https://charts.jenkins.io
helm repo update
kubectl create ns jenkins
helm upgrade --install jenkins -n jenkins -f jenkins-values.yaml jenkinsci/jenkins
可以通过如下命令查看管理员密码,登录上去后请到系统设置里配置和k8s集群的连接,因为我们需要用pod动态做jenkins slave节点。
kubectl exec --namespace jenkins -it svc/jenkins -c jenkins -- /bin/cat /run/secrets/
chart-admin-password && echo
另外我还创建了IngressRoute以便能外部访问
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: jenkins-ingress
namespace: jenkins
spec:
entryPoints:
- websecure
routes:
- match: Host(`jenkins.infra.yourdomain.com`)
kind: Rule
services:
- name: jenkins
port: 8080
tls:
certResolver: le
2
使用helm在k8s里搭建ArgoCD
我修改了chart中默认的一些value,chart-values.yaml内容如下
redis-ha: enabled: true
controller:
enableStatefulSet: trueserver:
extraArgs: ["--insecure"]
autoscaling:
enabled: true
minReplicas: 2
repoServer:
autoscaling:
enabled: true
minReplicas: 2
然后使用这个value创建helm应用
helm upgrade --install argo argo/argo-cd --version 3.29.0 -f chart-values.yaml -n argocd
同样创建了IngressRoute以便能外部访问
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: argocd-server
namespace: argocd
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`argocd.infra.youedata.com`)
priority: 10
services:
- name: argo-argocd-server
port: 80
- kind: Rule
match: Host(`argocd.infra.youedata.com`) && Headers(`Content-Type`, `application/grpc`)
priority: 11
services:
- name: argo-argocd-server
port: 80
scheme: h2c
tls:
certResolver: le
3
Jenkins pipeline的创建示例
CI流水线通过Jenkinsfile描述,纳入版本控制中作为Infrastructure as code。
下面的示例代码中,描述的过程包括拉取代码、运行测试、构建容器、归档容器等操作,需要根据团队实际情况调整。
podTemplate(label: 'jnlp-slave', cloud: 'kubernetes', containers: [ containerTemplate(name: 'maven', image: 'maven:3.6.0-jdk-8', ttyEnabled: true, command: 'cat'), containerTemplate(name: 'docker', image: 'docker:19.03.1-dind', privileged: true) ]) { node("jnlp-slave") { withCredentials([usernamePassword(credentialsId: 'harbor', passwordVariable: 'harborPassword', usernameVariable: 'harborUser')]) { stage('clone code') { git credentialsId: 'yq_key', url: 'ssh://git@gitlab-gitlab-shell.gitlab.svc.cluster.local:443/root/springbootstarter.git' env.imageTag = sh (script: 'git rev-parse --short HEAD ${GIT_COMMIT}', returnStdout: true).trim() sh 'echo $imageTag' container('maven') { stage('test') { sh 'mvn clean package } } }
container('docker') { stage('build image') { sh ''' docker login -u $harborUser -p $harborPassword https://harbor.infra.yourdomain.com docker build -t harbor.infra.yourdomain.com/library/springboot-starter-demo:$imageTag . docker images ''' } stage('push image to registry') { sh 'docker push harbor.infra.yourdomain.com/library/springboot-starter-demo:$imageTag' } } }
}
}
这里定义了一个Pod Template,因为我们的Jenkins利用了k8s的能力做横向扩展,每一个Job都跑在一个Pod内,相当于一个slave节点,这样可以多个构建任务同步进行。在上述的Pod内,安装了maven还有docker,因此后续的job可以使用maven进行jar包构建、测试等,也能够有构建docker容器的能力。
4
ArgoCD部署应用
Argo CD中可以利用GUI界面创建任务,最终生成的是一个CRD对象,类型是Application。因此我们其实也可以直接编写这个Application类型的yaml文件,然后提交到k8s中。下面的示例里部署了一个harbor的应用,这个应用是一个helm工程。里面的destination表明了要把它部署到哪里去,因为示例里是要部署到ArgoCD所在的k8s集群,因此这里的server就直接是用的默认的k8s service。然后source指定是要让Argo去追踪的仓库地址。它会去pull需要部署的内容,然后和k8s中的状态进行sync,具体的syncPolicy中可以指定你期望的行为。
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: harbor
spec:
destination:
name: ''
namespace: harbor
server: 'https://kubernetes.default.svc'
source:
path: ''
repoURL: 'https://charts.bitnami.com/bitnami'
targetRevision: 11.1.6
chart: harbor
helm:
parameters:
- name: global.storageClass
value: ceph-rbd
- name: harborAdminPassword
value: devops
- name: service.type
value: ClusterIP
- name: persistence.persistentVolumeClaim.chartmuseum.size
value: 1000Gi
- name: persistence.persistentVolumeClaim.jobservice.size
value: 50Gi
- name: persistence.persistentVolumeClaim.registry.size
value: 1000Gi
- name: persistence.persistentVolumeClaim.trivy.size
value: 50Gi
- name: service.tls.enabled
value: 'false'
project: infra
syncPolicy:
automated:
prune: true
selfHeal: false
syncOptions:
- CreateNamespace=true
完成上述工作后,我们就可以在Argo CD的界面里看到应用部署的状态。可以很直观地看到这个应用都生成了哪些对象,拓扑结构如何。也可以很方便地了解到应用部署的状态是否健康。
最后,我想强调的是,我们在做GitOps的时候,工具只是其中很小的一部分,更重要的是我们的工程实践,比如有没有一个好的代码提交习惯,代码的分支管理是否对于持续集成足够友好,测试策略是否能够提前反馈问题,基础设施即代码有没有做等等。只有做好了这些实践,那么才能够最大限度地发挥工具的价值。