软件技术学习笔记

个人博客,记录软件技术与程序员的点点滴滴。

基于Docker、K8S和GitLab的云原生微服务Auto-DevOps

目前,微服务是比较流行且成熟的后端架构。这几年来,K8S、云原生与Service Mesh也比较火热。

去年开始接触到K8S,然后开始学习互联网架构与云原生,前段时间有空在自己的笔记本中搭建开放环境。环境准备如下:

  • DNS服务:阿里云域名服务,解析qinzhiqiang.cn的子域名到本地VM IP
  • 宿主:Windows 10 专业版 + Hyper-V,CPU四核八线程,内存32G,使用Docker CLI
  • VM1:Ubuntu 18.04 Server + Docker,提供Docker daemon、Docker registry服务,内存2G
  • VM2:Ubuntu 18.04 Server + Docker,提供GitLab服务,内存8G
  • VM3:Ubuntu 18.04 Server + Docker,提供K8S服务,内存16G

1. DNS解析

因后面的Docker register使用HTTPS比非安全HTTP还方便,不需在每个运行环境中修改docker-daemon中的配置,因此,使用域名服务给各个VM分配独立的域名。

虚拟机 域名 IP
VM1 docker.qinzhiqiang.cn, registry.qinzhiqiang.cn 192.168.80.112
VM2 gitlab.qinzhiqiang.cn 192.168.80.113
VM3 k8s.qinzhiqiang.cn, *.k8s.qinzhiqiang.cn 192.168.80.96

阿里云DNS解析

可以看到一个特殊的通配符域名*.k8s.qinzhiqiang.cn,因为GitLab Auto-DevOps需要给每个微服务自动分配子域名与配置Ingress完成自动化测试。

如果没有自己的独立域名,需要新建虚拟机VM0提供DNS服务,然后宿主与其他VM均使用VM0提供的DNS服务。该方案参考Bind9,比阿里云DNS服务配置麻烦多了。

2. CA证书准备

XCA签发证书

使用XCA软件新建CA Root,再给*.qinzhiqiang.cn颁发HTTPS服务证书。 最后,在宿主与VM1/2/3中安装CA root.

Ubuntu中导入根证书如下:

sudo cp ksqzq_CA.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
sudo service docker restart

Windows 10 导入证书,可自行百度搜索。

3. 在Ubuntu中安装Docker

3个VM均需要安装Docker,后续所有的服务软件都在Docker中运行,称为“Everything in Docker”。

安装部署,参考“Ubuntu 18.04中安装Docker”。

4. VM1中开放Docker daemon TCP 2375端口与安装Docker Registry

在纯粹的GitLab Auto DevOps中,因GitLab本身也提供registry服务,该步骤中的“安装Docker registry”是不需要。但为了我们不使用Auto DevOps流水线时,也可手写CI/CD脚本部署微服务,需要一个位置统一存储Docker Images。

开放2375端口,参考“开放Docker Daemon远程访问端口(TCP 2375)

安装Docker Registry,参考“Ubuntu中部署Docker Registry与Docker Registry UI”。

在K8s集群中安装时:将Docker Registry作为StatefullSets单副本安装到K8s中,将Docker Registry UI作为Deployments单或多副本安装到K8s中,使用Nignx Ingress接入外部流量。

5. Windows 10 中安装Docker Desktop,仅使用Docker CLI

下载 Docker Desktop 并安装,关闭自动运行。添加以下环境变量,仅使用Docker CLI:

变量名 变量值
DOCKER_API_VERSION 1.39
DOCKER_DRIVER none
DOCKER_HOST tcp://docker.qinzhiqiang.cn:2375

仅为本地开放环境,没有使用mTLS通信。一个开发环境,Docker本身就是C/S架构,作为本地开发环境,我们只需要一个 Docker daemon就够用了,节省内存资源。

C:\Users\your-user-name>docker ps
CONTAINER ID        IMAGE                             COMMAND                  CREATED             STATUS              PORTS                                      NAMES
f378a2fdd041        nginx:alpine                      "nginx -g 'daemon of…"   5 weeks ago         Up 24 hours         0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp   nginx
78ae84157ec2        joxit/docker-registry-ui:static   "/bin/sh -c entrypoi…"   5 weeks ago         Up 24 hours         80/tcp                                     registry-ui
df0b6293ce9d        registry:2                        "/entrypoint.sh /etc…"   3 months ago        Up 24 hours         5000/tcp                                   registry

6. 安装K8S单机实例

参考“Ubuntu中部署Kubernetes (K8s)集群”文章,再仔细阅读其中的“6. 部署K8s单例”。

7. 安装 GitLab 服务 (Everything in Docker)

GitLab也使用Docker容器提供服务,同时需要把 SSH 22端口、HTTP 80端口、HTTPS 443端口引入到GitLab容器中。 先修改 VM2 的SSH端口号到2222:

sudo nano /etc/ssh/sshd_config
  Port 2222

sudo reboot

再准备数据目录与HTTPS密钥对:

# make dir
sudo mkdir -p /srv/gitlab/config/ssl
sudo mkdir -p /srv/gitlab/data
sudo mkdir -p /srv/gitlab/logs

# Copy SSL key to config dir
***@gitlab-ce:/srv/gitlab/config/ssl$ ls
  gitlab.qinzhiqiang.cn.crt
  gitlab.qinzhiqiang.cn.key

最后,启动服务:

sudo docker run --detach \
  --hostname gitlab.qinzhiqiang.cn \
  --env GITLAB_OMNIBUS_CONFIG="external_url 'https://gitlab.qinzhiqiang.cn/'; registry_external_url 'https://gitlab.qinzhiqiang.cn:4567'; gitlab_rails['lfs_enabled'] = true; nginx['redirect_http_to_https'] = true; registry_nginx['redirect_http_to_https'] = true; mattermost_nginx['redirect_http_to_https'] = true;" \
  --publish 4567:4567 --publish 443:443 --publish 80:80 --publish 22:22 \
  --name gitlab \
  --restart always \
  --volume /srv/gitlab/config:/etc/gitlab \
  --volume /srv/gitlab/logs:/var/log/gitlab \
  --volume /srv/gitlab/data:/var/opt/gitlab \
  gitlab/gitlab-ce:latest

等待启动完成,就可以登录 https://gitlab.qinzhiqiang.cn 了。

GitLab Admin Area

在K8s集群中安装时:请参考 https://docs.gitlab.com/charts/#installing-gitlab-using-the-helm-chart

8. GitLab 添加 K8S Cluster

先需要把 ksqzq_CA.crt 添加到 GitLab-Runner镜像中,避免Runner无法访问 GitLab 与 GitLab-registry(自动带的Docker镜像服务)。

定制GitLab-Runner镜像的Dockerfile如下,详情见 https://github.com/kinsprite/custom-auto-devops/tree/master/gitlab-runner ,后续仍需定制 auto-build-imageauto-deploy-image

FROM gitlab/gitlab-runner:alpine-v12.1.0

RUN apk add --no-cache wget ca-certificates
#COPY ksqzq_CA.crt /usr/local/share/ca-certificates/
RUN wget -O /usr/local/share/ca-certificates/ksqzq_CA.crt http://192.168.3.3:8080/ksqzq_CA.crt
RUN update-ca-certificates

在K8S的VM3中准备GitLab-Runner镜像,执行以下命令:

## or use custom image in K8S
docker pull registry.qinzhiqiang.cn/custom-gitlab-runner:v12.0.0
docker tag registry.qinzhiqiang.cn/custom-gitlab-runner:v12.0.0 gitlab/gitlab-runner:alpine-v12.0.0
# then use kubectl to  delete the runner POD

添加K8S集群:

  1. 使用GitLab的root用户登录,添加已有的K8S集群,参考:https://docs.gitlab.com/ee/user/project/clusters/index.html#add-existing-kubernetes-cluster 。其中的Base domain填写为k8s.qinzhiqiang.cn
  2. 安装GitLab Apps,参考:https://docs.gitlab.com/ee/user/project/clusters/index.html#installing-applications 。安装 Helm Tiller、Ingress、Prometheus、GitLab Runner。

如果要在K8S中玩Serverless,需要安装Istio与Knative,至少需要64G的内存。而32G的内存,只能玩普通的K8S + Auto DevOps。

GitLab K8S

已安装的GitLab Apps如下:

xxx@k8s-single:~$ kubectl get pods -n gitlab-managed-apps
NAME                                                     READY   STATUS    RESTARTS   AGE
ingress-nginx-ingress-controller-7d44688bf-fgk5t         1/1     Running   5          38d
ingress-nginx-ingress-default-backend-66645696bf-kl24n   1/1     Running   5          38d
prometheus-kube-state-metrics-744949b679-5bx42           1/1     Running   9          39d
prometheus-prometheus-server-646888949c-zw8lb            2/2     Running   16         39d
runner-gitlab-runner-7b7ff54f7-pxvvt                     1/1     Running   4          38d
tiller-deploy-5c68968c89-g579d                           1/1     Running   4          38d

转发VM3的80/443端口到Ingress的NodePort:

方法一:使用iptables转发

xxx@k8s-single:~$ kubectl get svc -n gitlab-managed-apps ingress-nginx-ingress-controller
NAME                               TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE
ingress-nginx-ingress-controller   LoadBalancer   10.100.16.253   <pending>     80:30209/TCP,443:31918/TCP   38d

#!/bin/bash
sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 30209
sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 31918

方法二:使用kubectl port-forward (没有放到后台运行,Ctrl+C 可以中断运行)

sudo kubectl port-forward --address 192.168.80.96  -n gitlab-managed-apps service/ingress-nginx-ingress-controller  80:80

未使用iptables持久化,重新启动 VM3 之后,需要重新执行端口转发命令。

在Windows中验证端口转发是否成功:

C:\Users\your-user-name>curl -v http://unknown.k8s.qinzhiqiang.cn
* Rebuilt URL to: http://unknown.k8s.qinzhiqiang.cn/
*   Trying 192.168.80.96...
* TCP_NODELAY set
* Connected to unknown.k8s.qinzhiqiang.cn (192.168.80.96) port 80 (#0)
> GET / HTTP/1.1
> Host: unknown.k8s.qinzhiqiang.cn
> User-Agent: curl/7.55.1
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Server: nginx/1.13.8
< Date: Tue, 08 Oct 2019 02:07:15 GMT
< Content-Type: text/plain; charset=utf-8
< Content-Length: 21
< Connection: keep-alive
< Strict-Transport-Security: max-age=15724800; includeSubDomains;
<
default backend - 404* Connection #0 to host unknown.k8s.qinzhiqiang.cn left intact

default backend返回404说明配置成功了。

9. GitLab中创建Auto-DevOps项目

  1. 创建的项目之后,与普通的项目差别在于Settings->CI/CD->Auto DevOps中需要勾选Default to Auto DevOps pipeline
  2. 使用GoLang创建微服务,只能使用唯一的5000端口(目前GitLab只支持5000端口,另外实现Serverless时也只能使用一个端口)。具体代码参考:https://github.com/kinsprite/golang-auto-devops
// main.go
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
    jsoniter "github.com/json-iterator/go"
)

var json = jsoniter.ConfigCompatibleWithStandardLibrary

func main() {
    println("Hello 'golang-auto-devops'")
    mainRouter().Run(":5000") // 监听并在 0.0.0.0:5000 上启动服务
}

func mainRouter() *gin.Engine {
    engine := gin.New()

    engine.GET("/healthz", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "OK",
        })
    })

    engine.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "pong",
        })
    })

    return engine
}
# Dockerfile

# build
FROM golang:1.12.9-alpine3.10 as build

# default port of auto devops is 5000
ENV PORT 5000
EXPOSE 5000

RUN mkdir /app
ADD . /app

ENV GOPROXY https://goproxy.io
ENV GIN_MODE release

WORKDIR  /app
RUN go mod vendor
RUN go build -mod=vendor -tags=jsoniter -o golang-auto-devops .


# release
FROM alpine:3.10
RUN mkdir /app
COPY --from=build /app/golang-auto-devops /app/golang-auto-devops

WORKDIR  /app
CMD ["/app/golang-auto-devops"]

  1. 定制CI配置文件。因为我们的CA证书是自己颁发的,需要使用使用定制的构建与部署镜像。如下的.gitlab-ci.yml中,使用共享的Docker tcp://docker.qinzhiqiang.cn:2375 ,可以加速构建,但手写CI脚本时需避免镜像名称重复;Helm部署K8S微服务时,需要设置探针的参数,否则微服务不断重启;因为使用比较新的go module,所以不能使用默认的 test 流水线,只能定制新的 test 流水线。
# .gitlab-ci.yml
include:
  - template: Auto-DevOps.gitlab-ci.yml

services:
  - name: docker:dind
    entrypoint: ["env", "-u", "DOCKER_HOST"]
    command: ["dockerd-entrypoint.sh"]

variables:
  DOCKER_HOST: tcp://docker.qinzhiqiang.cn:2375/
  DOCKER_DRIVER: overlay2
  # See https://github.com/docker-library/docker/pull/166
  DOCKER_TLS_CERTDIR: ""
  # Database
  POSTGRES_ENABLED: "false"
  # use custom test
  TEST_DISABLED: "true"
  PERFORMANCE_DISABLED: "true"
  # healthz
  HELM_UPGRADE_EXTRA_ARGS: "--set livenessProbe.path=/healthz --set readinessProbe.path=/healthz"

before_script:
  #- cd $CI_PROJECT_DIR
  - export GOPROXY=https://goproxy.io
  - export GIN_MODE=release
  - export CGO_ENABLED=0
  - export GO111MODULE=on

stages:
  - test
  - build
  - deploy  # dummy stage to follow the template guidelines
  - review
  - dast
  - staging
  - canary
  - production
  - incremental rollout 10%
  - incremental rollout 25%
  - incremental rollout 50%
  - incremental rollout 100%
  - performance
  - cleanup

.auto-deploy:
  image: "kinsprite/gitlab-auto-deploy-image:v0.1.0"

build:
  image: "kinsprite/gitlab-auto-build-image:stable"

golang_test:
  stage: test
  image: golang:1.12.9-alpine3.10
  script:
    - go mod vendor
    - go vet -mod=vendor .
    - go test -mod=vendor .
  only:
    - branches
    - tags

  1. 修改代码,push到master分支,可以看得流水线正常执行了。

GitLab Pipeline

下载流水线的归档文档artifacts.zip,查看其中的environment_url.txt,可以看得URL内容如下:

http://ksqzq-golang-auto-devops.k8s.qinzhiqiang.cn

在Windows中,验证微服务是否自动部署成功:

C:\Users\your-user-name>curl http://ksqzq-golang-auto-devops.k8s.qinzhiqiang.cn/ping
{"message":"pong"}

更高级的金丝雀发布、性能测试等功能,GitLab-CE版本没有,只有付费的高级许可才拥有这些功能,但其原理也是GitLab CI/CD的Pipeline,我们也可以在后面添加自己的Stage.

10. 非Auto DevOps时,如何使用CI/CD部署到K8S

没有使用Auto DevOps时,自由度比较高,但是所需完成的事情也比较多。完整代码,见:https://github.com/kinsprite/gitlab-hello-world 。

  1. 准备条件,在K8S Cluster中安装Tiller,参考:https://helm.sh/docs/using_helm/#installing-tiller
  2. 添加独立的GitLab Runner。使用Docker版本的,参考:https://docs.gitlab.com/runner/install/docker.html 和 https://docs.gitlab.com/runner/register/index.html#docker 。配置Runner时,需要把CA证书、Docker login信息、docker.sock等信息携带到CI job容器中。在VM2上执行以下命令:

GitLab Runner

sudo mkdir -p /srv/gitlab-runner/config sudo cp ~/ksqzq_CA.crt /srv/gitlab-runner/config/ca.crt

sudo docker run -d –name gitlab-runner –restart always
-v /srv/gitlab-runner/config:/etc/gitlab-runner
-v /var/run/docker.sock:/var/run/docker.sock
-v /root/.docker:/root/.docker
gitlab/gitlab-runner:latest

sudo docker exec -it gitlab-runner cp /etc/gitlab-runner/ca.crt /usr/local/share/ca-certificates/ca.crt sudo docker exec -it gitlab-runner update-ca-certificates sudo docker restart gitlab-runner

sudo docker exec -it gitlab-runner gitlab-runner register

Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com ) https://gitlab.qinzhiqiang.cn …. …. Please enter the gitlab-ci tags for this runner (comma separated): docker …. ….

GitLab Runner settings

xxx@gitlab-ce:~$ sudo cat /srv/gitlab-runner/config/config.toml concurrent = 1 check_interval = 0

[session_server] session_timeout = 1800

[[runners]] name = “ab2be2113dc2” url = “https://gitlab.qinzhiqiang.cn/" token = “BXy6xxxxxxxxxxxxxxxxxx” executor = “docker” [runners.custom_build_dir] [runners.docker] tls_verify = false image = “golang:1.12.7-alpine3.10” privileged = false disable_entrypoint_overwrite = false oom_kill_disable = false disable_cache = false volumes = ["/etc/gitlab-runner:/etc/gitlab-runner”, “/var/run/docker.sock:/var/run/docker.sock”, “/root/.docker:/root/.docker”, “/cache”] shm_size = 0 [runners.cache] [runners.cache.s3] [runners.cache.gcs] [runners.custom] run_exec = ""

Update the runner’s config

xxx@gitlab-ce:~$ sudo nano /srv/gitlab-runner/config/config.toml xxx@gitlab-ce:~$ sudo docker restart gitlab-runner

3. 到源代码目录,使用`draft create`命令创建`charts/`文件,后续可以使用`Helm`部署到K8S。Draft文档,见 https://draft.sh/ 。
4. 编写CI配置文件。主要关注构建过程的`docker build`、`docker push`和部署过程的`helm init --client-only`、`helm upgrade`。其中的 tags 务必与步骤2的tags一致。
```yaml
# .gitlab-ci.yml
# This file is a template, and might need editing before it works on your project.
image: golang:1.12.7-alpine3.10

variables:
  # Please edit to your GitLab project
  REPO_NAME: gitlab.qinzhiqiang.cn/ksqzq/gitlab-hello-world
  CI_DEBUG_TRACE: "false"

before_script:
  - cd $CI_PROJECT_DIR
  - export GOPROXY=https://goproxy.io
  - export GIN_MODE=release
  - export CGO_ENABLED=0
  - export GO111MODULE=on

stages:
  - test
  # - build
  - docker_image
  - deploy

testing_job:
  stage: test
  tags: [docker]
  script:
    - go mod vendor
    - go vet -mod=vendor .
    - go test -mod=vendor .

compiling_job:
  stage: build
  tags: [docker]
  script:
    - go mod vendor
    - go build -mod=vendor -tags=jsoniter -o gitlab-hello-world .
  #artifacts:
  #  paths:
  #    - gitlab-hello-world

building_job:
  image: docker:stable
  stage: docker_image
  tags: [docker]
  script:
    - docker build -t registry.qinzhiqiang.cn/gitlab-hello-world:$CI_COMMIT_SHA .
    - docker push registry.qinzhiqiang.cn/gitlab-hello-world:$CI_COMMIT_SHA

deploying_job:
  image: dtzar/helm-kubectl:latest
  stage: deploy
  tags: [docker]
  script:
    - mkdir /root/.kube
    - cp $KUBE_CONFIG_FILE /root/.kube/config
    - helm init --client-only
    - helm upgrade gitlab-hello-world charts/gitlab-hello-world --install --set image.repository=registry.qinzhiqiang.cn/gitlab-hello-world --set image.tag=$CI_COMMIT_SHA
  1. 在GitLab项目 ksqzq/gitlab-hello-world 的 Settings->CI/CD->Variables 中,添加变量 KUBE_CONFIG_FILE,其内容就是K8S部署之后生成的config文件。GitLab CI/CD Variables

完成以上步骤,push代码到master分支,我们可以看得流水线跑的结果成功了。 GitLab CI/CD Result OK

部署任务详情如下: GitLab CI/CD Deploy Job

11. 总结

GitLab配合Docker、K8S可以非常顺利地实现Auto DevOps。其强大之处,主要在于GitLab Runner可以运行Docker。有了Docker,各自不同的任务都可以完美隔离运行。CI/CD时,我们依赖的工具集主要有:docker、helm。

从头到尾实现Auto DevOps,需要了解以下知识:SSH、DNS域名解析、RSA证书颁发、CA证书安装、XCA软件使用、Nginx配置、Docker、Docker Registry、K8S、GitLab、GitLab Runner、Helm、Draft、Chart、Iptables。

搭建真实的DevOps环境时,我们应该把所有的Docker Registry、GitLab都放到K8s中,这样可以统一遥测日志、指标等信息,原则是一切都上云。

本文的样例代码,只使用GoLang语言。相关源码地址:

  1. 定制镜像: https://github.com/kinsprite/custom-auto-devops
  2. Auto DevOps微服务样例: https://github.com/kinsprite/golang-auto-devops
  3. 定制CI/CD微服务样例:https://github.com/kinsprite/gitlab-hello-world