From 0456a789d4f2faf8021c5e14cd77ee67a2023415 Mon Sep 17 00:00:00 2001 From: yangchuanhu Date: Sat, 4 May 2019 22:02:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0devops=E6=96=87=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SUMMARY.md | 2 +- ...24.Deployment \344\275\277\347\224\250.md" | 2 +- docs/66.devops.md | 373 +++++++++++++----- gitlab-ci-k8s-demo | 1 - 4 files changed, 282 insertions(+), 96 deletions(-) delete mode 160000 gitlab-ci-k8s-demo diff --git a/SUMMARY.md b/SUMMARY.md index 52aa78d..f9d4c65 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -99,5 +99,5 @@ * [Harbor](docs/63.Harbor.md) * [Gitlab](docs/64.Gitlab.md) * [Gitlab CI](docs/65.Gitlab CI.md) -* [Devops](docs/66/devops.md) +* [Devops](docs/66.devops.md) diff --git "a/docs/24.Deployment \344\275\277\347\224\250.md" "b/docs/24.Deployment \344\275\277\347\224\250.md" index a1023e4..58228ee 100644 --- "a/docs/24.Deployment \344\275\277\347\224\250.md" +++ "b/docs/24.Deployment \344\275\277\347\224\250.md" @@ -199,7 +199,7 @@ REVISION CHANGE-CAUSE 上生产时,我们最好通过设置Deployment的`.spec.revisionHistoryLimit`来限制最大保留的`revision number`,比如15个版本,回滚的时候一般只会回滚到最近的几个版本就足够了。其实`rollout history`中记录的`revision`都和`ReplicaSets`一一对应。如果手动`delete`某个ReplicaSet,对应的`rollout history`就会被删除,也就是还说你无法回滚到这个`revison`了。 -`roolout history`和`ReplicaSet`的对应关系,可以在`kubectl describe rs $RSNAME`返回的`revision`字段中得到,这里的`revision`就对应着`roolout history`返回的`revison`。 +`rollout history`和`ReplicaSet`的对应关系,可以在`kubectl describe rs $RSNAME`返回的`revision`字段中得到,这里的`revision`就对应着`rollout history`返回的`revison`。 同样我们可以使用下面的命令查看单个`revison`的信息: diff --git a/docs/66.devops.md b/docs/66.devops.md index 6316c6e..4315556 100644 --- a/docs/66.devops.md +++ b/docs/66.devops.md @@ -330,12 +330,14 @@ container('构建 Docker 镜像') { credentialsId: 'dockerhub', usernameVariable: 'DOCKER_HUB_USER', passwordVariable: 'DOCKER_HUB_PASSWORD']]) { - echo "3. 构建 Docker 镜像阶段" - sh """ - docker login ${dockerRegistryUrl} -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD} - docker build -t ${image}:${imageTag} . - docker push ${image}:${imageTag} - """ + container('docker') { + echo "3. 构建 Docker 镜像阶段" + sh """ + docker login ${dockerRegistryUrl} -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD} + docker build -t ${image}:${imageTag} . + docker push ${image}:${imageTag} + """ + } } } ``` @@ -505,99 +507,284 @@ stage('运行 Kubectl') { ``` -第五阶段:运行 Helm 工具,就是直接使用 Helm 来部署应用了,现在有了上面的基本的资源对象了,现在要创建 Chart 模板就相对容易了,首先确保我们本地安装有 Helm Client,使用下面的命令创建项目: +第五阶段:运行 Helm 工具,就是直接使用 Helm 来部署应用了,现在有了上面的基本的资源对象了,要创建 Chart 模板就相对容易了,Chart 模板仓库地址:[https://github.com/cnych/polling-helm](https://github.com/cnych/polling-helm),我们可以根据`values.yaml`文件来进行自定义安装,模板中我们定义了可以指定使用外部数据库服务或者内部独立的数据库服务,具体的我们可以去看模板中的定义。首先我们可以先使用这个模板在集群中来测试下。首先在集群中 Clone 上面的 Chart 模板: ```shell -$ helm create polling-helm -Creating polling-helm -$ tree polling-helm -polling-helm -├── Chart.yaml -├── charts -├── templates -│   ├── NOTES.txt -│   ├── _helpers.tpl -│   ├── deployment.yaml -│   ├── ingress.yaml -│   └── service.yaml -└── values.yaml - -2 directories, 7 files +$ git clone https://github.com/cnych/polling-helm.git ``` -然后将上面的 k8s.yaml 文件我们拆分成四个文件放置到 templates 目录下面,把 polling-server 分成 deployment.yaml 和 service.yaml 文件,而数据库的我们单独创建一个文件夹,然后放置在文件夹下面: -```shell -$ tree polling-helm/templates -polling-helm/templates -├── NOTES.txt -├── _helpers.tpl -├── database -│   ├── database-svc.yaml -│   └── database-ss.yaml -├── deployment.yaml -├── ingress.yaml -└── service.yaml - -1 directory, 7 files +然后我们使用内部的数据库服务,新建一个 custom.yaml 文件来覆盖 values.yaml 文件中的值: +```yaml +persistence: + enabled: true + persistentVolumeClaim: + database: + storageClass: "database" + +database: + type: internal + internal: + database: "polling" + # 数据库用户 + username: "polling" + # 数据库用户密码 + password: "polling321" ``` -然后就可以定制模板了,首先针对主要的 deployment.yaml 文件,把所有能够定制的数据都通过 values 或者 template 来进行渲染: +可以看到我们这里使用了一个名为`database`的 StorgeClass 对象,所以还得创建先创建这个资源对象: ```yaml -apiVersion: extensions/v1beta1 -kind: Deployment +apiVersion: storage.k8s.io/v1 +kind: StorageClass metadata: - name: {{ include "polling-helm.fullname" . }} - labels: - app: {{ include "polling-helm.name" . }} - chart: {{ include "polling-helm.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -spec: - replicas: {{ .Values.replicaCount }} - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - template: - metadata: - labels: - app: {{ include "polling-helm.name" . }} - release: {{ .Release.Name }} - spec: - restartPolicy: Always - imagePullSecrets: - - name: myreg - containers: - - name: {{ .Chart.Name }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - containerPort: 8080 - name: api - env: - - name: DB_HOST - value: {{ .Values.db.host }} - - name: DB_PORT - value: "3306" - - name: DB_NAME - value: polling_app - - name: DB_USER - value: polling - - name: DB_PASSWORD - value: polling321 - resources: -{{ toYaml .Values.resources | indent 12 }} - {{- with .Values.nodeSelector }} - nodeSelector: -{{ toYaml . | indent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: -{{ toYaml . | indent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: -{{ toYaml . | indent 8 }} - {{- end }} + name: database +provisioner: fuseim.pri/ifs +``` + +然后我们就可以在 Chart 根目录下面安装应用,执行下面的命令: +```shell +$ helm upgrade --install polling -f custom.yaml . --namespace course +Release "polling" does not exist. Installing it now. +NAME: polling +LAST DEPLOYED: Sat May 4 23:31:42 2019 +NAMESPACE: course +STATUS: DEPLOYED + +RESOURCES: +==> v1/Pod(related) +NAME READY STATUS RESTARTS AGE +polling-polling-api-6b699478d6-lqwhw 0/1 ContainerCreating 0 0s +polling-polling-ui-587bbfb7b5-xr2ff 0/1 ContainerCreating 0 0s +polling-polling-database-0 0/1 Pending 0 0s + +==> v1/Secret +NAME TYPE DATA AGE +polling-polling-database Opaque 1 0s + +==> v1/Service +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +polling-polling-api ClusterIP 10.109.19.220 8080/TCP 0s +polling-polling-database ClusterIP 10.98.136.190 3306/TCP 0s +polling-polling-ui ClusterIP 10.108.170.43 80/TCP 0s + +==> v1beta2/Deployment +NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE +polling-polling-api 1 1 1 0 0s +polling-polling-ui 1 1 1 0 0s + +==> v1/StatefulSet +NAME DESIRED CURRENT AGE +polling-polling-database 1 1 0s + +==> v1beta1/Ingress +NAME HOSTS ADDRESS PORTS AGE +polling-polling-ingress ui.polling.domain 80 0s + + +NOTES: +1. Get the application URL by running these commands: + http://ui.polling.domain + +You have new mail in /var/spool/mail/root +``` + +> 注意我们这里安装也是使用的`helm upgrade`命令,这样有助于安装和更新的时候命令统一。 + + +安装完成后,查看下 Pod 的运行状态: +```shell +$ kubectl get pods -n course +NAME READY STATUS RESTARTS AGE +polling-polling-api-6b699478d6-lqwhw 1/1 Running 0 3m +polling-polling-database-0 1/1 Running 0 3m +polling-polling-ui-587bbfb7b5-xr2ff 1/1 Running 0 3m ``` +然后我们可以在本地`/etc/hosts`里面加上`http://ui.polling.domain`的的映射,这样我们就可以通过这个域名来访问我们安装的应用了,可以注册、登录、发表投票内容了: + +![polling app](https://bxdc-static.oss-cn-beijing.aliyuncs.com/images/oblr3.png) + +这样我们就完成了使用 Helm Chart 安装应用的过程,但是现在我们使用的包还是直接使用的 git 仓库中的,平常我们正常安装的时候都是使用的 Chart 仓库中的包,所以我们需要将该 Chart 包上传到一个仓库中去,比较幸运的是我们的 Harbor 也是支持 Helm Chart 包的。我们可以选择手动通过 Harbor 的 Dashboard 将 Chart 包进行上传,也可以通过使用`Helm Push`插件: +```shell +$ helm plugin install https://github.com/chartmuseum/helm-push +Downloading and installing helm-push v0.7.1 ... +https://github.com/chartmuseum/helm-push/releases/download/v0.7.1/helm-push_0.7.1_linux_amd64.tar.gz + +Installed plugin: push +``` + +当然我们需要首先将 Harbor 提供的仓库添加到 helm repo 中,由于是私有仓库,所以在添加的时候我们需要添加用户名和密码: +```shell +$ helm repo add course https://registry.qikqiak.com/chartrepo/course --username= --password= +"course" has been added to your repositories +``` + +这里的 repo 的地址是**/chartrepo/**,Harbor 中每个项目是分开的 repo,如果不提供项目名称,则默认使用`library`这个项目。 + +> 需要注意的是如果你的 Harbor 是采用的自建的 https 证书,这里就需要提供 ca 证书和私钥文件了,否则会出现证书校验失败的错误`x509: certificate signed by unknown authority`。我们这里是通过`cert-manager`为 Harbor 提供的一个信任的 https 证书,所以没有指定 ca 证书相关的参数。 + +然后我们将上面的`polling-helm`这个 Chart 包上传到 Harbor 仓库中去: +```shell +$ helm push polling-helm course +Pushing polling-0.1.0.tgz to course... +Done. +``` + +这个时候我们登录的 Harbor 仓库中去,查看 course 这个项目下面的`Helm Charts`就可以发现多了一个 polling 的应用了: + +![helm chart](https://bxdc-static.oss-cn-beijing.aliyuncs.com/images/96e57.png) + +我们也可以在右下角看到有添加仓库和安装 Chart 的相关命令。 + + +到这里 Helm 相关的工作就准备好了。那么我们如何在 Jenkins Pipeline 中去使用 Helm 呢?我们可以回顾下,我们平时的一个 CI/CD 的流程:开发代码 -> 提交代码 -> 触发镜像构建 -> 修改镜像tag -> 推送到镜像仓库中去 -> 然后更改 YAML 文件镜像版本 -> 使用 kubectl 工具更新应用。 + +现在我们是不是直接使用 Helm 了,就不需要去手动更改 YAML 文件了,也不需要使用 kubectl 工具来更新应用了,而是只需要去覆盖下 helm 中的镜像版本,直接 upgrade 是不是就可以达到应用更新的结果了。我们可以去查看下 chart 包的 values.yaml 文件中关于 api 服务的定义: +```yaml +api: + image: + repository: cnych/polling-api + tag: 0.0.7 + pullPolicy: IfNotPresent +``` + +我们是不是只需要将上面关于 api 服务使用的镜像用我们这里 Jenkins 构建后的替换掉就可以了,这样我们更改上面的最后`运行 Helm`的阶段如下: +```groovy +stage('运行 Helm') { + container('helm') { + echo "更新 polling 应用" + sh """ + helm upgrade --install polling polling --set persistence.persistentVolumeClaim.database.storageClass=database --set database.type=internal --set database.internal.database=polling --set database.internal.username=polling --set database.internal.password=polling321 --set api.image.repository=${image} --set api.image.tag=${imageTag} --set imagePullSecrets[0].name=myreg --namespace course + """ + } +} +``` + +当然我们可以将需要更改的值都放入一个 YAML 之中来进行修改,我们这里通过`--set`来覆盖对应的值,这样整个 API 服务的完整 Jenkinsfile 文件如下所示: +```groovy +def label = "slave-${UUID.randomUUID().toString()}" + +def helmLint(String chartDir) { + println "校验 chart 模板" + sh "helm lint ${chartDir}" +} + +def helmInit() { + println "初始化 helm client" + sh "helm init --client-only --stable-repo-url https://mirror.azure.cn/kubernetes/charts/" +} + +def helmRepo(Map args) { + println "添加 course repo" + sh "helm repo add --username ${args.username} --password ${args.password} course https://registry.qikqiak.com/chartrepo/course" + + println "更新 repo" + sh "helm repo update" + + println "获取 Chart 包" + sh """ + helm fetch course/polling + tar -xzvf polling-0.1.0.tgz + """ +} + +def helmDeploy(Map args) { + helmInit() + helmRepo(args) + + if (args.dry_run) { + println "Debug 应用" + sh "helm upgrade --dry-run --debug --install ${args.name} ${args.chartDir} --set persistence.persistentVolumeClaim.database.storageClass=database --set database.type=internal --set database.internal.database=polling --set database.internal.username=polling --set database.internal.password=polling321 --set api.image.repository=${args.image} --set api.image.tag=${args.tag} --set imagePullSecrets[0].name=myreg --namespace=${args.namespace}" + } else { + println "部署应用" + sh "helm upgrade --install ${args.name} ${args.chartDir} --set persistence.persistentVolumeClaim.database.storageClass=database --set database.type=internal --set database.internal.database=polling --set database.internal.username=polling --set database.internal.password=polling321 --set api.image.repository=${args.image} --set api.image.tag=${args.tag} --set imagePullSecrets[0].name=myreg --namespace=${args.namespace}" + echo "应用 ${args.name} 部署成功. 可以使用 helm status ${args.name} 查看应用状态" + } +} + + +podTemplate(label: label, containers: [ + containerTemplate(name: 'maven', image: 'maven:3.6-alpine', command: 'cat', ttyEnabled: true), + containerTemplate(name: 'docker', image: 'docker', command: 'cat', ttyEnabled: true), + containerTemplate(name: 'helm', image: 'cnych/helm', command: 'cat', ttyEnabled: true) +], volumes: [ + hostPathVolume(mountPath: '/root/.m2', hostPath: '/var/run/m2'), + hostPathVolume(mountPath: '/home/jenkins/.kube', hostPath: '/root/.kube'), + hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock') +]) { + node(label) { + def myRepo = checkout scm + def gitCommit = myRepo.GIT_COMMIT + def gitBranch = myRepo.GIT_BRANCH + def imageTag = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() + def dockerRegistryUrl = "registry.qikqiak.com" + def imageEndpoint = "course/polling-api" + def image = "${dockerRegistryUrl}/${imageEndpoint}" + + stage('单元测试') { + echo "1.测试阶段" + } + stage('代码编译打包') { + try { + container('maven') { + echo "2. 代码编译打包阶段" + sh "mvn clean package -Dmaven.test.skip=true" + } + } catch (exc) { + println "构建失败 - ${currentBuild.fullDisplayName}" + throw(exc) + } + } + container('构建 Docker 镜像') { + withCredentials([[$class: 'UsernamePasswordMultiBinding', + credentialsId: 'dockerhub', + usernameVariable: 'DOCKER_HUB_USER', + passwordVariable: 'DOCKER_HUB_PASSWORD']]) { + container('docker') { + echo "3. 构建 Docker 镜像阶段" + sh """ + docker login ${dockerRegistryUrl} -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD} + docker build -t ${image}:${imageTag} . + docker push ${image}:${imageTag} + """ + } + } + } + stage('运行 Helm') { + withCredentials([[$class: 'UsernamePasswordMultiBinding', + credentialsId: 'dockerhub', + usernameVariable: 'DOCKER_HUB_USER', + passwordVariable: 'DOCKER_HUB_PASSWORD']]) { + container('helm') { + echo "4. [INFO] 开始 Helm 部署" + helmDeploy( + dry_run : false, + name : "polling", + chartDir : "polling", + namespace : "course", + tag : "${imageTag}", + image : "${image}", + username : "${DOCKER_HUB_USER}", + password : "${DOCKER_HUB_PASSWORD}" + ) + echo "[INFO] Helm 部署应用成功..." + } + } + } + } +} +``` + +由于我们没有将 chart 包放入到 API 服务的代码仓库中,这是因为我们这里使用的 chart 包涉及到两个应用,一个 API 服务,一个是前端展示的服务,所以我们这里是通过脚本里面去主动获取到 chart 包来进行安装的,如果 chart 包跟随代码仓库一起管理当然就要简单许多了。 + +现在我们去更新 Jenkinsfile 文件,然后提交到 gitlab 中,然后去观察下 Jenkins 中的构建是否成功,我们重点观察下 Helm 阶段: + +![jenkins helm console](https://bxdc-static.oss-cn-beijing.aliyuncs.com/images/yfilp.png) + +当然我们还可以去做一些必要的判断工作,比如根据分支判断是否需要自动部署等等,同样也可以切换到 Blue Occean 界面查看构建结果。 + +![jenkins blue occean](https://bxdc-static.oss-cn-beijing.aliyuncs.com/images/sq24p.jpg) + + +现在大家可以尝试去修改下代码,然后提交代码到 gitlab 上,观察下 Jenkins 是否能够自动帮我们完成整个 CI/CD 的过程。 + +作业:现在还有一个前端展示的项目:[http://git.qikqiak.com/course/polling-app-client.git](http://git.qikqiak.com/course/polling-app-client.git),大家针对这个项目使用上面的 gitlab + jenkins + harbor + helm 来完成一个 Jenkins Pipeline 流水线的编写,尝试去修改下前端页面内容,看是否能够生效。 + diff --git a/gitlab-ci-k8s-demo b/gitlab-ci-k8s-demo deleted file mode 160000 index 62ec25b..0000000 --- a/gitlab-ci-k8s-demo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 62ec25b629f8a7c174c40916c38293694bcf6e5d -- GitLab