GitOps理念也是最近比较火的话题。
什么是GitOps
GitOps 是一种快速、安全的方法,可供开发或运维人员维护和更新运行在 Kubernetes 或其他声明式编排框架中的复杂应用。
GitOps的四项基本原则
- 通过声明的方式描述系统。类似于现在推荐的脚本化pipeline,terraform等,集中在代码里声明所需要的资源,而不是通过页面的方式进行配置。
- 系统的目标状态通过git版本控制。版本可追溯,可回滚。
- 对目标状态改变批准后自动应用到系统中。自动化程度高,一键应用。
- 持续比较环境中的状态和代码版本里的状态,触发告警,k8s的自愈能力也将得到应用。
GitOps的好处
- 开发者可以使用熟悉的工具 Git 去发布新功能,而无需了解复杂的部署交付流程,提升生产力。
- 使用 Git 工作流管理集群,使得所有变更需要review,这样满足合规性需求,提升系统的安全与稳定性。
- 借助 Git 的还原(revert)、分叉(fork)功能,可以实现稳定且可重现的回滚,系统更加可靠。
- 由于 GitOps 可以为infra、application、Kubernetes 插件的部署变更提供了统一的模型yaml,因此我们可以在整个组织中实现一致的E2E工作流。CICD和Ops工作都可以通过Git来实现,一致性和规范性程度化高。
- 可以借助 Git 内置的安全特性。
本文主要描述运用GitOps理念,通过check in code的方式,使得生产jenkins能够adopt新的change。
主要流程如下:
本文中几个重要的角色:Helm,jhipster-registry,Git,jenkins。
1. Jhipster-registry(配置中心)
https://github.com/jhipster/jhipster-registry
JHipster-registry具有三个主要功能:
- 它是一个Eureka服务,用作应用程序的发现服务。这就是JHipster处理所有应用程序的路由,负载均衡和可伸缩性的方式。
- 它是一个Spring Cloud配置服务,为所有应用程序运行时提供配置。
- 它还是一台管理服务器,具有用于监视和管理应用程序的仪表板。
从官网上download下来源码,参考README文档启动。
JHipster Registry是Spring Config Server:启动应用程序时,它们将首先连接到JHipster Registry以获取其配置。网关和微服务都是如此。
此配置是Spring Boot配置,就像在JHipsterapplication-*.yml
文件中找到的配置一样,但是它存储在中央服务器中,因此更易于管理。
启动时,网关和微服务应用程序将查询Registry的配置服务器,并用在那里定义的属性覆盖其本地属性。
我们在central-config中新增application-jaymz.yml
那么我们访问localhost:8761的时候,输入默认的admin:admin, 进入config页面,在profile里面输入jaymz,就能得到我们刚刚配置的信息。
这样配置有什么好处呢?
- 配置信息集中管理。将散落在各个系统中的配置信息统一放置在配置中心里。任何有权限访问的人都能得到全景图。
- 版本控制。针对不同的用处(系统环境/用户),可以声明几份配置yml文件,根据profile来进行区分配置yml的使用场景。
2. Helm
helm的作用想必大家也知道,Helm是把Kubernetes资源(比如deployments、services或 ingress等) 打包到一个chart中,而chart被保存到chart仓库。可以通过chart仓库可用来存储和分享chart。Helm使发布可配置,支持发布应用配置的版本管理,简化了Kubernetes部署应用的版本控制、打包、发布、删除、更新等操作。我们可以在下面的路径中找到需要的服务chart,比如:jenkins,sonarqube,grafana等。
https://github.com/helm/charts/tree/master/stable
我们通过helm的方式获取jenkins chart,然后在k8s环境中启动该服务。
chart.yaml:
url: https://github.com/helm/charts/tree/master/stable
chart: jenkins
values.yaml文件可以根据https://github.com/helm/charts/tree/master/stable/jenkins进行自定义设置。比如设置它的代理,初始化安装的插件等。
chart(服务模版)有了,values(实例配置)也有了。通过helm命令,helm install chart即可。如果我们想在jenkins实例中做些改变,比如升级插件,增加配置等,我们通常的做法就是helm upgrade。或者删除老的,重新安装新的。
以上是手动做法。
手动能够实现,那么我们是不是可以寻求自动化的方式,达到这样的效果:只要jenkins的values.yaml文件有变动,一旦代码check in,就会直接应用到instance里。逻辑:jenkins job 扫描整个repo,发现如果有新的service加入,那么会进入该folder下读取chart和values文件,再使用helm add repo,helm install的方式将该服务启动在k8s集群中。如果检测的服务已经安装过,那么就先删除老得服务,再和新的安装步骤一样,将服务安装/升级进来。或许你会问服务删除了,数据不就丢失了吗?因此我们又多加了一项pvc的处理,将服务的数据持久化到硬盘上。服务是根据namespace来隔离的。namespace的名字则是由用户定义的folder名字。当我们删除资源的时候,直接删除整个namespace,确保资源清理干净。文件结构如下:
folder/chart.yaml,folder/values.yaml。
扫描逻辑示例如下:
import java.time.ZonedDateTime
def kubeconfigFileId='kubeconfig-gitops-devops'
def proxy='http://jaymz.com:8080'
timestamps {
podTemplate(
containers: [
containerTemplate(name: 'jnlp',
image: 'localhost:5000/jenkins/jnlp-slave:3.35-5',
args: '${computer.jnlpmac} ${computer.name}'),
containerTemplate(name: 'storage',
image: 'localhost:5000/ubuntu:latest',
ttyEnabled: true, command: 'cat',runAsUser: '1000', runAsGroup: '1000'),
],
volumes: [
nfsVolume(mountPath: '/etc/nfs', serverAddress: 'localhost', serverPath: '/root/jaymz/'),
],
) {
node(POD_LABEL) {
checkout scm
withCredentials([kubeconfigFile(credentialsId: kubeconfigFileId, variable: 'KUBECONFIG')]){
def parallel_services
stage('find service(s) to install'){
parallel_services=findFiles(glob: '*/chart.yaml')
.collect {it.path.substring(0, it.path.indexOf('/'))}
.findAll {
// 判断是否需要安装服务,可通过git log,helm status来获取集群中该服务的deploy时间
echo "Install ${release}: ${should_install}"
return should_install;
}
.collectEntries {
def release=it
def chart_yaml=readYaml file: "${release}/chart.yaml"
def repo=chart_yaml.repo
def repo_url=chart_yaml.url
def chart=chart_yaml.chart
return [release, {
touch file: "${release}/values.yaml"
withEnv(["RELEASE=${release}", "REPO=${repo}", "REPO_URL=${repo_url}","CHART=${chart}",
"https_proxy=${proxy}","STORAGE_ROOT=/etc/nfs"
]){
stage("storage"){
//为每个服务创建对应的文件夹
}
stage("prepare namespace"){
kubernetesDeploy(kubeconfigId: kubeconfigFileId,
configs: "namespace.yaml",
enableConfigSubstitution: true
)
}
stage("install ${release}"){
container('helm'){
def kube_templates = findFiles(glob: "${RELEASE}/templates/*.yaml")
if (kube_templates.size()>0) {
kubernetesDeploy(kubeconfigId: kubeconfigFileId, // REQUIRED
configs: "${RELEASE}/templates/*.yaml", // REQUIRED
enableConfigSubstitution: true
)
}
if (repo){
sh '''
helm delete ${RELEASE} -n ${RELEASE} || true
helm repo add ${REPO} ${REPO_URL}
helm upgrade ${RELEASE} ${REPO}/${CHART} -f ${RELEASE}/values.yaml -n ${RELEASE} -i --force --kubeconfig ${KUBECONFIG}
'''
} else {
sh '''
helm delete ${RELEASE} -n ${RELEASE} || true
helm upgrade ${RELEASE} ${CHART} --repo ${REPO_URL} -f ${RELEASE}/values.yaml -n ${RELEASE} -i --force --kubeconfig ${KUBECONFIG}
'''
}
}
}
}
}
]
}
}
stage('install services'){
parallel parallel_services
}
}
}
}
}
写到这里我们基本上实现了大图中左边循环。那么接下来来探索右边循环。
每个项目肯定都会有自己的configuration,我们不可能把所有的配置都放置在一个repo中,这违反了工程代码的就近原则,而且维护起来也不是特别的方便。那么如何做到配置在不同的repo,但是运行时传入到配置中心中呢?
如上图所示,我们可以在jhipster-registry中的application.yml中新增以上配置,新的repo:https://jaymz.com/ops/springboot-jaymz.git则是我项目的仓库。在该仓库中我们把配置信息存在路径为config的文件夹下,通过“searchPaths来查找”,config文件夹里的文件配置和配置中心的规则一样application-profile.yaml
, 这样我们就把项目的配置信息发送给了配置中心了。
那么当配置中心中被提交了一份新的jenkins 配置。用户希望根据这个新的配置生成一套属于该用户的jobs,怎么实现?
首先我们要编写seed job。这个job可以帮助我们在jenkins中生成其他的一系列的job(比如:重置机器,部署数据库,部署环境等)。
seed job的思路是,传入用户配置的profile,application,configserver url的值,seed job扫描整个repo下存在的Jenkinsfile,根据配置yaml文件中定义的key value值,传入到Jenkinsfile中,并在jenkins里生成对应的job。
那么当我们触发测试job的时候,该job会首先读取配置中心的数据,然后根据读取的值进行处理相应的任务。目前应用到场景如下:根据不同的profile,指定不同的收件人,部署不同的环境,运行不同的autoamtion cases。
seed job生成其他job主要用到了jobDsl。并把配置中心的相关参数传入到每个Jenkinsfile中。
然后Jenkinsfile通过httpRequest的方式去获取配置信息。
def application = env.application
def url = "${env.configserver}/config/${env.branch}/${application}-${env.profile}.${format}"
println("config server url $url")
httpRequest ignoreSslErrors: true, quiet: true, url: url, authentication: configserver_credentials, outputFile: "config-server.yml"
综上,我们通过helm(管理服务instance),Jhipster-registry(配置中心),jenkins(jobs)来实现了GitOps。一键提交代码,就像多米诺骨牌一样,引起了一连串可控的变化。