无论是大型软件公司还是小型软件公司,现在每天都要部署数千个容器实例,这种扩缩容的复杂性是他们必须要管理的。本文介绍了如何将 Kubernetes
纳入到现有的传统 CI/CD 管道中,并实现服务的高可用性,以及随时在生产环境中进行代码变更。

基于容器的微服务架构改变了开发和运维团队测试和部署现代应用程序 /
服务的方式。容器通过简化应用程序的扩缩容和部署来帮助公司实现现代化,但容器也创建了一个全新的基础设施生态系统,从而引入了新的挑战和更多的复杂性。

无论是大型软件公司还是小型软件公司,现在每天都要部署数千个容器实例,这种扩缩容的复杂性是他们必须要管理的。那么他们是怎么做的呢?

1 秘诀就是 Kubernetes

Kubernetes 最初由 Google 开发,是一个开源的容器编排平台,旨在自动化容器化应用程序的部署、扩缩容和管理。

在本教程中,我们将介绍如何将 Kubernetes 纳入到现有的传统 CI/CD
管道中,并实现服务的高可用性,以及随时(是的,任何时候都不会影响服务)在生产环境中进行代码变更。

使用到的工具

本文是基于你对如下主题有基本 / 良好的理解的基础上的。

  • Kubernetes(我们的服务在 Kubernetes 上运行)

  • Jenkins 以及 Jenkins 共享库(CI/CD 工具)

  • GIT(SCM 工具)

  • HAProxy(网络负载均衡器)

  • Ansible(配置管理工具)

CI/CD 安装

  • Jenkins 服务器——与 Docker、Ansible 和 Kubectl 一起安装(将 Kube 管理配置.kube 从 K8s master 复制到 Jenkins 服务器的主目录中)

  • 具有一主两从三个节点的 K8s 集群

  • HAProxy 服务器

使用 K8s 的 CI/CD

2 GIT & Jenkins

让我们来详细介绍一下如何配置所有这些不同的工具,并使其能够完美地运行。

我们的示例应用程序,名为 shoppingapp,在 GitHub 中有三个存储库,每个库都具有特定的微服务。

  • shoppingapp-home:首页微服务

  • shoppingapp-kids :儿童版页面微服务

  • shoppingapp-mens:男士版页面微服务

类似地,针对这三个库我们有三个 Jenkins 管道作业,并在存储库中设置了 GitHub webhook,以便在有新提交时自动启动构建。

Jenkins 管道作业

我们来看下 shoppingapp-home 作业的配置。

Jenkins 管道配置

在 shoppingapp-home 的管道作业中,仓库 URL 指向 shoppingapp-home 的 GitHub
存储库。类似地,另外两个管道作业也指向各自的存储库。

下面是 shoppingapp-home 存储库中的 Jenkinsfile。


@Library('jenkins-shared-library') _
pipeline {
   agent any
   environment {
       app = 'shoppingapp'
       service = 'shoppingapp-home'
       registryCredential = 'dockerhub'
       dockerImage = ''
       imageid = "deepanmurugan/shoppingapp-home:$BUILD_NUMBER"
   }
   stages {
       stage('Build') {
            steps {
        script {
            dockerImage = dockerbuild(imageid)
        }
        }
        }
       stage('Test') {
           steps {
        testcase()
           }
       }
       stage('Publish') {
           steps{
               script {
            imagepush(imageid)
                   }
           }
       }
       stage('Pull Playbook Repo') {
        steps {
        dir('/tmp/ansible-playbooks/') {
            gitcheckout(
                branch: "master",
                repoUrl: "https://github.com/deepanmurugan/Ansible_Playbook.git"
            )
        }
    }
       }
       stage ('Deploy') {
           steps {
           dir('/tmp/ansible-playbooks/') {
               script{
            deploytok8s(imageid,app,service)
               }
           }
           }
       }
   }
}

shoppingapp-home 存储库的
Jenkinsfile 以下是管道作业中的不同步骤。作业声明了几个变量,app——指我们的应用程序名,service——指我们的服务名,registryCredentials——是我们保存在
Jenkins 中的 dockerhub 用户名和密码,imageid——带有标签的 Docker 容器镜像名,这里的标签将是 Jenkins
的内部版本号。

在上面的 Jenkinsfile 中,我使用了共享库(如果你不熟悉共享库的话,请参考 Jenkins 网站)。

在“Build”(构建)阶段,我在共享库中调用了函数dockerbuild,并将 imageid
作为参数进行传递。以下是dockerbuild函数的定义。


def call(String dockerImage) {
  script {
    docker.build "${dockerImage}"
  }
}

Build 阶段

它仅使用我们传递的 imageid 作为参数调用 docker.build,就会触发docker build -t imageid, 并基于
Dockerfile 创建一个 Docker 镜像。我们稍后再看 Dockerfile。

下一步是“Test”(测试),此时我们可以运行测试用例。我目前不执行任何测试用例,因为这是一个非常基础的应用程序。



def call() {
    sh """
        echo "Testing the docker built image"
    """
}

Test 阶段

接下来,我们进入“Publish”(发布)阶段,该阶段将再次调用imagepush函数,其中 imageid 作为共享库中的参数。该函数基本上要登录到
Docker Hub,并将镜像推送到 Docker Hub 中。


def call(String dockerImage) {
  echo "${dockerImage}"
  withCredentials([usernamePassword(credentialsId: 'dockerhub', usernameVariable: 'hubUsername', passwordVariable: 'hubPassword')]) {
        sh """
            docker login --username="${hubUsername}" --password="${hubPassword}"
            docker push "${dockerImage}"
        """
    }
}

Publish 阶段

下一步是“Pull Playbook Repo”,仅克隆存储库,该库中包含了我们所有的 playbook。


def call(Map stageParams){
  checkout([$class: 'GitSCM', branches: [[name: stageParams.branch]], userRemoteConfigs: [[credentialsId: 'github_repo', url: stageParams.repoUrl]]])
}

Pull Playbook Repo 阶段

接下来是“Deploy”(部署)阶段,该阶段触发 playbook 将容器部署到 Kubernetes 集群中。


def call(String dockerImage, String app, String service) {
    sh """
        ansible-playbook deploy_k8s.yml --extra-vars \"image_id=${dockerImage} app_name=${app} service_name=${service}\"
    """
}

Deploy 阶段

现在,我们对如何开发和维护每个微服务的存储库、如何配置 Jenkins 管道来创建 Docker 镜像并将镜像推送到 Docker Hub 以及触发
Ansible Playbook 将容器部署到 K8s 集群都已经相当清楚了。那我们来看一下它的 Dockerfile 是什么样的。

3 Dockerize 应用程序


FROM python:3.7.3-alpine3.9
RUN mkdir -p /app
WORKDIR /app
COPY ./src/requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt
COPY ./src/ /app
ENV FLASK_APP=server.py
CMD flask run -h 0.0.0.0 -p 5000

Dockerfile

Dockerfile 使用 python:3.7.3-alpine3.9 作为基础镜像,我们安装了一个 flask
应用程序来打印某些文本。该应用程序运行在端口 5000 上。

4 使用 Ansible Playbook 来进一步接管

下面是 Ansible Playbook 的 deploy_k8s.yml,它是由 Jenkins 管道作业触发的。


- hosts: localhost
  user: ubuntu
  tasks:
    - name: Deploy the service
      k8s:
        state: present
        definition: "{{ lookup('template', 'k8s/{{app_name}}/{{service_name}}/deployment.yml') | from_yaml }}"
        validate_certs: no
        namespace: default
    - name: Deploy the application
      k8s:
        state: present
        validate_certs: no
        namespace: default
        definition: "{{ lookup('template', 'k8s/{{app_name}}/{{service_name}}/service.yml') | from_yaml }}"
    - name: Deploy the Ingress
      k8s:
        state: present
        validate_certs: no
        namespace: default
        definition: "{{ lookup('template', 'k8s/{{app_name}}/common/ingress.yml') | from_yaml }}"

使用 Ansible 部署 K8s 组件

5 Kubernetes 集群

现在我们来看一下 K8s 集群。集群中有一个主节点和两个工作节点。


ubuntu@kube-master:~$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
kube-master Ready master 10d v1.19.4
kube-worker1 Ready <none> 10d v1.19.4
kube-worker2 Ready <none> 9d v1.19.4

此外,我们还安装了 Traefik Ingress 控制器,作为集群中两个副本的部署(deployment)。


ubuntu@kube-master:~$ kubectl get pods -n kube-system|grep ingress
traefik-ingress-controller-6b7f594d46–5jqzq 1/1 Running 0 7d12h
traefik-ingress-controller-6b7f594d46-vvfch 1/1 Running 0 8d

为了无缝地部署 / 扩展我们的服务,我们以特定的格式组织了 Ansible Playbook 库。

K8s 服务定义的目录结构

每个服务都有两个特定于该服务的文件 deployment 和 service,common 目录中则包含了 ingress 配置定义。


apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: "{{ app_name }}"
  name: {{ service_name }}-deloyment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: "{{ app_name }}"
      task: "{{ service_name }}"
  template:
    metadata:
      labels:
        app: "{{ app_name }}"
        task: "{{ service_name }}"
    spec:
      containers:
      - name: {{ service_name }}-pod
        image: "{{ image_id }}"
        imagePullPolicy: Always
        ports:
        - containerPort: 5000
      imagePullSecrets:
        - name: dockerhubsecret
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

deployment.yml

上面的 deployment 文件是通用的,它从 Jenkins 管道作业中获取所有传输到 deploy_k8s.yml 的变量,并将其传输到该 K8s 的
deployment.yml 中。因此,若要用更多的微服务来扩展应用程序,只需在 Ansible Playbook 库中创建适当的目录结构以及具有
ingress 配置的 Jenkins 管道,即可实现完美地运行。

在创建部署之后,我们需要创建一个服务来公开该部署。该服务将端口 80 映射到 pod 的端口 5000。


apiVersion: v1
kind: Service
metadata:
  name: {{ service_name }}-service
spec:
  selector:
    app: "{{ app_name }}"
    task: "{{ service_name }}"
  ports:
    - protocol: TCP
      port: 80
      targetPort: 5000
      name: http

service.yml

Deployment.yml 和 Service.yml 文件对于 shoppingapp 服务下的所有微服务都是通用的。

另外,我们看一下 common/ingress.yml 中 ingress 配置。ingress 的主机(host)名是
app.shoppingapp.com,它具有基于路径的规则,可将流量重定向到特定的服务。



apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: {{ app_name }}-ingress
  annotations:
    kubernetes.io/ingress.class: traefik
    traefik.frontend.rule.type: PathPrefixStrip
spec:
  rules:
  - host: app.shoppingapp.com
    http:
      paths:
      - path: /home
        backend:
          serviceName: shoppingapp-home-service
          servicePort: http
      - path: /kids
        backend:
          serviceName: shoppingapp-kids-service
          servicePort: http
      - path: /mens
        backend:
          serviceName: shoppingapp-mens-service
          servicePort: http

ingress.yml

6 引入 HAProxy

另外,我还使用了 HAProxy 负载均衡器来平衡 K8s 集群中两个节点之间的流量。我已经将前端和后端添加到现有的 HAProxy
配置文件中。后端服务器是 K8s 集群中的工作节点。


frontend http_front
bind *:80
mode http
default_backend http_back

backend http_back
balance roundrobin
server kube 172.31.35.122:32365 check
server kube 172.31.40.13:32365 check

端口 32365 是 traefik-ingress-service 服务公开的端口。DNS 是为 ingress.yml 中提到的主机名
app.shoppingapp.com 创建的,该主机名会被解析为 HAProxy IP(我使用的是 AWS Route 53 内部域名来创建的域名和
DNS 记录)


ubuntu@jenkins_ansible:~$ kubectl describe svc traefik-ingress-service -n kube-system
Name: traefik-ingress-service
Namespace: kube-system
Labels: <none>
Annotations: <none>
Selector: k8s-app=traefik-ingress-lb
Type: NodePort
IP: 10.102.149.216
Port: web 80/TCP
TargetPort: 80/TCP
NodePort: web 32365/TCP
Endpoints: 10.244.1.99:80,10.244.3.19:80
Port: admin 8080/TCP
TargetPort: 8080/TCP
NodePort: admin 31387/TCP
Endpoints: 10.244.1.99:8080,10.244.3.19:8080
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>

7 真实的流程

对于这个为我们的微服务应用程序而创建的 CI/CD 管道,其所需的所有组件我们都已经了解了。现在,让我们将这些组件组装起来,以了解该流程的工作原理。

  • 开发人员提交代码到微服务仓库(shoppingapp-kids)

  • GitHub 中配置的 Webhook 通知 Jenkins 并触发相应的管道作业(shoppingapp kids)

  • 管道作业克隆 shoppingapp-kids 存储库,并开始执行 Jenkinsfile。

  • Jenkinsfile 中的构建阶段使用克隆的 shoppingapp-kids 存储库中的 Dockerfile 创建 Docker 镜像(deepanmurugan/shoppingapp-kids:21)。

  • 测试 Docker 镜像,如果测试通过,则将镜像推送到 Docker Hub。

  • 触发 Ansible Playbook,使用 app_name、service_name 以及 image_id 变量将 Docker 镜像部署到 K8s 集群。

  • Ansible Playbook 将读取所有与 deployment/servive/ingress 相关的 K8s 定义,并在集群中创建所需的组件。

8 向管道中添加新的微服务

假设我们有一个请求,要求在该架构中再添加一个名为 shoppingapp-ladies 的微服务。开发人员创建了一个名为 shoppingapp-
ladies 的存储库,并使用相同的 Docker 和 Jenkinsfile 提交了代码,唯一的变化是 Jenkinsfile 中的
service_name=shoppingapp-ladies 变量。

修改文件夹结构后,添加新的微服务

修改 common/ingress.yml,并添加一个名为 /ladies 的新路径,端点是 shoppingapp-ladies-service。


- path: /ladies
backend:
serviceName: shoppingapp-ladies-service
servicePort: http

拷贝现有的 Jenkins 管道作业,并创建一个名为 shoppingapp-ladies 的新管道作业。

Jenkins 管道作业列表

在新作业 shoppingapp-ladies 的管道中,只需添加新的存储库 URL,然后执行管道作业,仅此而已。它将创建一个新的部署和服务并修改
ingress。

ubuntu@jenkins_ansible:/opt/python$ curl app.shoppingapp.com/ladies
Welcome to shoppingapp — Ladies section — shoppingapp-ladies-deloyment-656f6f9d9f-dms8w

参考文章

https://medium.com/awsblogs/ci-cd-with-kubernetes-3c29e8073c38
https://mp.weixin.qq.com/s/nN1mgb0k27uCPX1CXMVP_g


 目录