如何使用Kubernetes,Ansible和Jenkins创建CD管道

CI / CD试图解决什么?

CI / CD是一个经常与其他术语(例如DevOps,Agile,Scrum和看板,自动化等)一起听到的术语。 有时,它被认为只是工作流的一部分,而没有真正了解它是什么或为什么采用它。 对于年轻的DevOps工程师来说,将CI / CD视为理所当然是很常见的,他们可能还没有看到软件发布周期的“传统”方式,因此无法欣赏CI / CD。

CI / CD代表持续集成/持续交付和/或部署。 未实现CI / CD的团队在创建新软件产品时必须经过以下阶段:

产品经理(代表客户的利益)提供产品应具有的必要功能以及产品应遵循的行为。 该文档必须尽可能详尽和具体。

具有业务分析师的开发人员通过编写代码,运行单元测试并将结果提交到版本控制系统(例如git)来开始处理应用程序。

开发阶段完成后,该项目将移至质量检查。 针对该产品运行了一些测试,例如用户接受测试,集成测试,性能测试。 在此期间,在QA阶段完成之前,不得对代码库进行任何更改。 如果应该有任何错误,则将它们传递给开发人员进行修复,然后将产品交给质量检查人员。

完成质量检查后,操作团队会将代码部署到生产中。

上述工作流程有许多缺点:

首先,从产品经理提出请求到产品准备生产为止,要花费很长时间。

对于开发人员来说,解决一个月或更长时间以来已经编写的代码中的错误非常困难。 请记住,只有在开发阶段结束并且QA阶段开始后才能发现错误。

当紧急代码更改(例如需要修复程序的严重错误)时,由于需要尽快部署,因此QA阶段通常会缩短。

由于不同团队之间几乎没有协作,因此人们会在出现错误时开始指责并互相指责。 每个人开始只关心自己的项目部分,而忽略了共同的目标。

CI / CD通过引入自动化解决了上述问题。 每次将代码更改推送到版本控制系统后,都将进行测试,然后将其进一步部署到登台/ UAT环境中,以进行进一步测试,然后再将其部署到生产环境中供用户使用。 自动化可确保整个过程快速,可靠,可重复且不易出错。

那么,什么是CI / CD?

有关此主题的完整书籍已经撰写完毕。 如何,为什么以及何时在您的基础架构中实施它。 但是,只要可能,我们总是更喜欢较少的理论,而是更多的实践。 话虽如此,以下是在提交代码更改后应执行的自动化步骤的简要说明:

持续集成(CI):第一步不包括质量检查。 换句话说,它不关注代码是否提供了客户端请求的功能。 相反,它可以确保代码的质量。 通过单元测试,集成测试,开发人员会很快收到有关代码质量问题的通知。 我们可以通过代码覆盖率和静态分析来进一步扩展测试,从而进一步保证质量。

用户验收测试:这是CD流程的第一部分。 在此阶段,将对代码执行自动测试,以确保其满足客户的期望。 例如,一个Web应用程序可以正常工作而不会引发任何错误,但是客户希望访问者在导航到主页之前,先找到要约的登陆页面。 当前代码将访问者直接带到主页,这与客户的需求有所不同。 UAT测试指出了此类问题。 在非CD环境中,这是人工QA测试人员的工作。

部署:这是CD流程的第二部分。 它涉及对将托管应用程序的服务器/吊舱/容器进行更改,以使其反映更新的版本。 这应该以自动化方式完成,最好通过诸如Ansible,Chef或Puppet之类的配置管理工具来完成。

什么是管道?

管道是一个非常简单的概念的幻想。 当您需要以某种顺序执行多个脚本以实现一个共同目标时,这些脚本统称为“管道”。 例如,在詹金斯(Jenkins)中,管道可能包含一个或多个阶段,必须全部完成才能使构建成功。 使用阶段有助于可视化整个过程,了解每个阶段花费了多长时间,并确定构建确切地在哪里失败。

实验室:为golang应用程序创建管道

在本实验中,我们正在构建持续交付(CD)管道。 我们正在使用一个用Go编写的非常简单的应用程序。 为了简单起见,我们将仅对代码运行一种类型的测试。 此实验的前提条件如下:

  • 正在运行的Jenkins实例。 它可以是云实例,虚拟机,裸机或Docker容器。 它必须可以从Internet公开访问,以便存储库可以通过Webhooks连接到Jenkins。
  • 映像注册表:您可以使用Docker Registry,基于云的产品(例如ECR或GCR) ,甚至可以使用自定义注册表。
  • GitHub上的帐户。 尽管在此示例中我们使用GitHub,但是该过程可以与其他存储库(如Bitbucket)一样进行较小的更改。

管道可以描述如下:

如何使用Kubernetes,Ansible和Jenkins创建CD管道_第1张图片

步骤01:应用程序文件

我们的示例应用程序将以“ Hello World”响应任何GET请求。 创建一个名为main.go的新文件,并添加以下行:

package main

import (
   "log"
   "net/http"
)

type Server struct {}

func (s *Server) ServeHTTP (w http.ResponseWriter, r *http.Request) {
   w.WriteHeader(http.StatusOK)
   w.Header().Set( "Content-Type" , "application/json" )
   w.Write([] byte ( `{"message": "hello world"}` ))
}

func main () {
   s := &Server{}
   http.Handle( "/" , s)
   log.Fatal(http.ListenAndServe( ":8080" , nil ))
}

由于我们正在构建CD管道,因此我们应该进行一些测试。 我们的代码非常简单,只需要一个测试用例即可。 确保在点击根网址时收到正确的字符串。 在同一目录中创建一个名为main_test.go的新文件,并添加以下行:

package main

import (
   "log"
   "net/http"
)

type Server struct {}

func (s *Server) ServeHTTP (w http.ResponseWriter, r *http.Request) {
   w.WriteHeader(http.StatusOK)
   w.Header().Set( "Content-Type" , "application/json" )
   w.Write([] byte ( `{"message": "hello world"}` ))
}

func main () {
   s := &Server{}
   http.Handle( "/" , s)
   log.Fatal(http.ListenAndServe( ":8080" , nil ))
}

我们还有其他一些文件可以帮助我们部署应用程序,这些文件名为:

Dockerfile

这是我们打包应用程序的地方:

FROM golang:alpine AS build-env
RUN mkdir / go /src/app && apk update && apk add git
ADD main. go / go /src/app/
WORKDIR / go /src/app
RUN CGO_ENABLED= 0 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o app .

FROM scratch
WORKDIR /app
COPY --from=build-env / go /src/app/app .
ENTRYPOINT [ "./app" ]

Dockerfile是一个多阶段文件,用于保持映像大小尽可能小。 它从基于golang:alpine的构建映像开始。 生成的二进制文件将用于第二张图像,这只是一个临时图像。 暂存映像不包含依赖项或库,仅包含启动应用程序的二进制文件。

服务

由于我们使用Kubernetes作为托管此应用程序的平台,因此我们至少需要一项服务和一个部署。 我们的service.yml文件如下所示:

apiVersion: v1
kind: Service
metadata:
  name: hello-svc
spec:
  selector:
    role: app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
      nodePort: 32000
  type : NodePort

这个定义没有什么特别的。 只是使用NodePort作为其类型的服务。 它将在任何群集节点的IP地址上的端口32000上进行侦听。 传入的连接将中继到端口8080上的Pod。对于内部通信,服务将侦听端口80。

部署

应用程序本身一旦被docker化,就可以通过Deployment资源部署到Kubernetes。 deploy.yml文件如下所示:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-deployment
  labels:
    role: app
spec:
  replicas: 2
  selector:
    matchLabels:
      role: app
  template:
    metadata:
      labels:
        role: app
    spec:
      containers:
      - name: app
        image: "{{ image_id }}"
        resources:
          requests:
            cpu: 10 m

关于此部署定义,最有趣的是映像部分。 我们不是使用硬编码图像名称和标签,而是使用一个变量。 稍后,我们将看到如何将该定义用作Ansible的模板,并通过命令行参数替换映像名称(以及部署的任何其他参数)。

剧本

在本实验中,我们使用Ansible作为部署工具。 还有许多其他方式来部署Kubernetes资源,包括Helm Charts ,但我认为Ansible是一个更容易的选择。 Ansible使用剧本来组织其说明。 我们的playbook.yml文件如下所示:

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

Ansible已经包含了k8s模块,用于处理与Kubernetes API服务器的通信。 因此,我们不需要安装kubectl,但确实需要一个有效的kubeconfig文件来连接到集群(稍后会详细介绍)。 让我们快速讨论一下这本剧本的重要部分:

该剧本用于将服务和资源部署到群集。

由于我们需要在执行时将数据快速注入到定义文件中,因此我们需要将定义文件用作模板,从那里可以从外部提供变量。

为此,Ansible具有查找功能,您可以在其中传递有效的YAML文件作为模板。 Ansible支持多种将变量注入模板的方法。 在这个特定的实验中,我们使用命令行方法。

步骤02:安装Jenkins,Ansible和Docker

让我们安装Ansible并使用它自动部署Jenkins服务器和Docker运行时环境。 我们还需要安装openshift Python模块以启用与Kubernetes的Ansible连接。

Ansible的安装非常简单; 只需安装Python并使用pip安装Ansible:

  1. 登录到Jenkins实例
  2. 安装Python 3,Ansible和openshift模块: sudo apt update && sudo apt install -y python3 && sudo apt install -y python3-pip && sudo pip3 install ansible && sudo pip3 install openshift
  3. 默认情况下,pip将二进制文件安装在用户主文件夹中的隐藏目录下。 我们需要将此目录添加到$ PATH变量中,以便我们可以轻松地调用命令: echo "export PATH=$PATH:~/.local/bin" >> ~/.bashrc && . ~/.bashrc echo "export PATH=$PATH:~/.local/bin" >> ~/.bashrc && . ~/.bashrc
  4. 安装部署Jenkins实例所需的Ansible角色: ansible-galaxy install geerlingguy.jenkins
  5. 安装Docker角色: ansible-galaxy install geerlingguy.docker
  6. 创建一个playbook.yaml文件并添加以下行:
  7. - hosts: localhost
      become: yes
      vars:
        jenkins_hostname: 35.238 .224 .64
        docker_users:
        - jenkins
      roles:
        - role: geerlingguy.jenkins
        - role: geerlingguy.docker
  8. 通过以下命令运行该剧本: ansible-playbook playbook.yaml. 注意,我们使用实例的公共IP地址作为Jenkins将使用的主机名。 如果使用DNS,则可能需要用实例的DNS名称替换它。 另外,请注意,在运行剧本之前,必须在防火墙(如果有)上启用端口8080。
  9. 几分钟后,应安装Jenkins。 您可以通过导航到计算机的IP地址(或DNS名称)并指定端口8080进行检查:
  10. 如何使用Kubernetes,Ansible和Jenkins创建CD管道_第2张图片
  11. 单击“登录”链接,并提供“ admin”作为用户名和“ admin”作为密码。 请注意,这些是我们使用的Ansible角色设置的默认凭据。 在生产环境中使用Jenkins时,您可以(并且应该)更改这些默认值。 这可以通过设置角色变量来完成。 您可以参考角色官方页面 。
  12. 您需要做的最后一件事是安装将在我们的实验室中使用的以下插件:git,管道,CloudBees Docker Build和Publish,GitHub。

    步骤03:配置Jenkins用户连接到集群

    如前所述,本实验假设您已经有一个Kubernetes
    集群运行。 为了使Jenkins连接到该集群,我们
    需要添加必要的kubeconfig文件。 在这个特定的实验室中,我们
    使用托管在Google Cloud上的Kubernetes集群,因此我们正在使用
    gcloud命令。 您的具体情况可能有所不同。 但是总的来说
    在这种情况下,我们必须将kubeconfig文件复制到Jenkins的用户目录中
    如下:

    class = "  language-yaml" >$ sudo cp ~ /.kube/ config ~jenkins/.kube/
    $ sudo chown class = "token punctuation" >-  R jenkinsclass = "token punctuation" >:  ~jenkins/.kube/ 

    请注意,您将在此处使用的帐户必须具有创建和管理“部署和服务”的必要权限。

    步骤04:创建Jenkins管道作业

    如何使用Kubernetes,Ansible和Jenkins创建CD管道_第3张图片

    创建一个新的Jenkins作业,然后选择Pipeline类型。 作业设置应如下所示:

    如何使用Kubernetes,Ansible和Jenkins创建CD管道_第4张图片
    如何使用Kubernetes,Ansible和Jenkins创建CD管道_第5张图片

    我们更改的设置是:

    • 我们使用Poll SCM作为构建触发器; 设置此选项将指示Jenkins定期(按* * * * *指示的每一分钟)检查Git存储库。 如果自上次轮询以来仓库已更改,则将触发作业。
    • 在管道本身中,我们指定了存储库URL和凭据。 分支是master。
    • 在本实验中,我们将所有作业的代码添加到Jenkins文件中,该文件与代码存储在同一存储库中。 本文稍后将讨论Jenkinsfile。

    步骤05:为GitHub和Docker Hub配置Jenkins凭据

    转到/ credentials / store / system / domain / _ / newCredentials并将凭据添加到两个目标。 确保为每个ID提供有意义的ID和描述,因为稍后将引用它们:

    如何使用Kubernetes,Ansible和Jenkins创建CD管道_第6张图片
    如何使用Kubernetes,Ansible和Jenkins创建CD管道_第7张图片

    步骤06:创建Jenkinsfile

    Jenkinsfile指导Jenkins如何构建,测试,docker化,发布和交付我们的应用程序。 我们的Jenkinsfile看起来像这样:

    pipeline {
       agent any
       environment {
           registry = "magalixcorp/k8scicd"
           GOCACHE = "/tmp"
       }
       stages {
           stage( 'Build' ) {
               agent {
                   docker {
                       image 'golang'
                   }
               }
               steps {
                   // Create our project directory.
                   sh 'cd ${GOPATH}/src'
                   sh 'mkdir -p ${GOPATH}/src/hello-world'
                   // Copy all files in our Jenkins workspace to our project directory.               
                   sh 'cp -r ${WORKSPACE}/* ${GOPATH}/src/hello-world'
                   // Build the app.
                   sh 'go build'              
               }    
           }
           stage( 'Test' ) {
               agent {
                   docker {
                       image 'golang'
                   }
               }
               steps {                
                   // Create our project directory.
                   sh 'cd ${GOPATH}/src'
                   sh 'mkdir -p ${GOPATH}/src/hello-world'
                   // Copy all files in our Jenkins workspace to our project directory.               
                   sh 'cp -r ${WORKSPACE}/* ${GOPATH}/src/hello-world'
                   // Remove cached test results.
                   sh 'go clean -cache'
                   // Run Unit Tests.
                   sh 'go test ./... -v -short'           
               }
           }
           stage( 'Publish' ) {
               environment {
                   registryCredential = 'dockerhub'
               }
               steps{
                   script {
                       def appimage = docker.build registry + ":$BUILD_NUMBER"
                       docker.withRegistry( '', registryCredential ) {
                           appimage.push()
                           appimage.push(' latest ')
                       }
                   }
               }
           }
           stage (' Deploy ') {
               steps {
                   script{
                       def image_id = registry + ":$BUILD_NUMBER"
                       sh "ansible-playbook  playbook.yml --extra-vars \"image_id=${image_id}\""
                   }
               }
           }
       }
    }
    
    
    

    该文件比看起来更容易构建。 管道基本上包含四个阶段:

    1. 构建是我们构建Go二进制文件的地方,并确保在构建过程中没有错误。
    2. 测试是我们应用简单的UAT测试以确保应用程序按预期工作的地方。
    3. 发布,在其中构建Docker映像并将其推送到注册表。 之后,任何环境都可以使用它。
    4. 部署,这是调用Ansible来联系Kubernetes并应用定义文件的最后一步。

    现在,让我们讨论这个Jenkinsfile的重要部分。

    前两个阶段大致相似。 他们俩都使用golang Docker映像来构建/测试应用程序。 让阶段通过已包含所有必要构建和测试工具的Docker容器运行始终是一个好习惯。 另一种选择是在主服务器或从服务器之一上安装这些工具。 当您需要针对不同的工具版本进行测试时,就会出现问题。 例如,也许因为我们的应用程序尚未准备好使用最新的Golang版本,所以我们可能想使用Go 1.9来构建和测试代码。 图像中包含所有内容,因此更改版本甚至图像类型就像更改字符串一样简单。

    Publish阶段(从第42行开始)首先指定一个环境变量,该变量将在以后的步骤中使用。 该变量指向我们在先前步骤中添加到Jenkins的Docker Hub凭据的ID。

    第48行:我们使用docker插件构建映像。 默认情况下,它在我们的注册表中使用Dockerfile,并将内部版本号添加为图像标签。 稍后,当您需要确定哪个Jenkins构建是当前运行的容器的来源时,这将非常重要。

    第49-51行:成功构建映像后,我们使用内部版本号将其推送到Docker Hub。 此外,我们在图像上添加了“最新”标签(第二个标签),以便我们允许用户在需要的情况下无需指定内部版本号即可拉取图像。

    第56-60行:在部署阶段,我们将部署和服务定义文件应用到集群。 我们使用前面讨论的剧本调用Ansible。 请注意,我们将image_id作为命令行变量传递。 该值将自动替换部署文件中的映像名称。

    测试我们的CD管道

    本文的最后一部分是我们实际对我们的工作进行测试的地方。 我们将代码提交到GitHub,并确保我们的代码在管道中移动直到到达集群:

    1. 添加我们的文件: git add *
    2. 提交更改: git commit -m "Initial commit"
    3. 推送到GitHub: git push
    4. 在Jenkins上,我们可以等待作业被自动触发,也可以单击“立即构建”。
    5. 如果作业成功,我们可以使用以下命令检查已部署的应用程序。
    6. 获取节点的IP地址:
    7. kubectl get nodes -o wide
      NAME                                          STATUS   ROLES    AGE   VERSION          INTERNAL-IP   EXTERNAL-IP     OS-IMAGE                             KERNEL-VERSION   CONTAINER-RUNTIME
      gke-security-lab- default -pool -46f 98c95-qsdj   Ready       7d    v1 .13 .11 -gke .9   10.128 .0 .59   35.193 .211 .74   Container-Optimized OS from Google   4.14 .145 +        docker: //18.9.7
    8. 现在,让我们向应用程序发起HTTP请求:
    9. $ curl 35.193 .211 .74 : 32000
      { "message" : "hello world" }

    好的,我们可以看到我们的应用程序运行正常。 让我们在代码中故意造成错误,并确保管道不会将错误的代码发送到目标环境:

    将应显示的消息更改为“ Hello World!”,请注意,我们将每个单词的首字母大写,并在末尾添加了感叹号。 由于我们的客户可能不希望该消息以这种方式显示,因此管道应在“测试”阶段停止。

    首先,让我们进行更改。 main.go文件现在应如下所示:

    package main
    
    import (
       "log"
       "net/http"
    )
    
    type Server struct {}
    
    func (s *Server) ServeHTTP (w http.ResponseWriter, r *http.Request) {
       w.WriteHeader(http.StatusOK)
       w.Header().Set( "Content-Type" , "application/json" )
       w.Write([] byte ( `{"message": "Hello World!"}` ))
    }
    
    func main () {
       s := &Server{}
       http.Handle( "/" , s)
       log.Fatal(http.ListenAndServe( ":8080" , nil ))
    }

    接下来,让我们提交并推送我们的代码:

    $ git add main. go
    $ git commit -m "Changes the greeting message"                                                                                                       
    [master 24 a310e] Changes the greeting message
     1 file changed, 1 insertion(+), 1 deletion(-)
    $ git push
    Counting objects: 3 , done.
    Delta compression using up to 4 threads.
    Compressing objects: 100 % ( 3 / 3 ), done.
    Writing objects: 100 % ( 3 / 3 ), 319 bytes | 319.00 KiB/s, done.
    Total 3 (delta 2 ), reused 0 (delta 0 )
    remote: Resolving deltas: 100 % ( 2 / 2 ), completed with 2 local objects.
    To https: //github.com/MagalixCorp/k8scicd.git
       7954e03 . .24 a310e  master -> master

    回到Jenkins,我们可以看到上一次构建失败了:

    如何使用Kubernetes,Ansible和Jenkins创建CD管道_第8张图片

    通过单击失败的作业,我们可以看到失败的原因:

    如何使用Kubernetes,Ansible和Jenkins创建CD管道_第9张图片

    我们的错误代码将永远不会进入目标环境。

    如果您喜欢这些,请在Magalix博客上查看我们的其他教程,注册我们的时事通讯,或立即尝试Magalix。

    TL; DR

    • CI / CD是遵循敏捷方法论的任何现代环境的组成部分。
    • 通过管道,您可以确保代码从版本控制系统到目标环境(测试/过渡/生产/等)的平稳过渡,同时应用所有必要的测试和质量控制实践。
    • 在本文中,我们有一个实际的实验室,我们在其中建立了一个持续交付管道来部署Golang应用程序。
    • 通过Jenkins,我们能够从存储库中提取代码,并使用相关的Docker映像进行构建和测试。
    • 接下来,我们对应用程序进行Dockerize并将其推入Docker Hub,因为它已经通过了测试。
    • 最后,我们使用Ansible将应用程序部署到运行Kubernetes的目标环境。
    • 使用Jenkins管道和Ansible,可以非常轻松灵活地更改工作流,而不会产生任何摩擦。 例如,我们可以在“测试”阶段添加更多测试,可以更改用于构建和测试代码的Go版本,还可以使用更多变量来更改部署和服务定义中的其他方面。
    • 最好的部分是,我们正在使用Kubernetes部署,这可以确保在更改容器映像时应用程序的停机时间为零。 这是可能的,因为部署默认情况下使用滚动更新方法来一次终止并重新创建一个容器。 仅当新容器启动且运行状况良好时,部署才会终止旧容器。

    先前发布在https://www.magalix.com/blog/create-a-ci/cd-pipeline-with-kubernetes-and-jenkins

    From: https://hackernoon.com/how-to-create-a-cd-pipeline-with-kubernetes-ansible-and-jenkins-i6c03yp2

    你可能感兴趣的:(运维,git,golang)