CI/CD是一个常常与其余术语(例如DevOps,Agile,Scrum和看板,自动化等)一块儿听到的术语。有时,它只是工做流的一部分而没有真正了解它是什么或为何采用它。对于年轻的DevOps工程师来讲,将CI/CD视为理所固然的事情很常见,他们可能尚未看到软件发布周期的“传统”方式,所以没法欣赏CI/CD。html
CI/CD表明持续集成/持续交付或部署。未实现CI/CD的团队在建立新软件产品时必须通过如下阶段:node
上述工做流程有许多缺点:python
CI/CD经过引入自动化解决了上述问题。每次将代码更改推送到版本控制系统后,都将进行测试,而后进一步部署到生产/UAT环境中,以进行进一步测试,而后再将其部署到生产环境中供用户使用。自动化可确保整个过程快速,可靠,可重复,而且不易出错。linux
咱们老是更喜欢较少的理论,更多的实践。话虽如此,如下是对一旦执行代码更改即应执行的自动化步骤的简要说明:git
Pipeline是一个很是简单的概念的幻想。当您须要以必定顺序执行多个脚本以实现共同目标时,这些脚本统称为“Pipeline”。例如,在Jenkins中,Pipeline可能包含一个或多个阶段,必须所有完成才能使构建成功。使用阶段有助于可视化整个过程,了解每一个阶段须要花费多长时间,并肯定构建在何处失败。github
在此实验中,咱们正在构建连续交付(CD)Pipeline。咱们正在使用一个用Go编写的很是简单的应用程序。为了简单起见,咱们将仅对代码运行一种类型的测试。该实验的前提条件以下:golang
Pipeline能够描述以下:docker
咱们的示例应用程序将对任何GET请求作出“ Hello World”响应。建立一个名为main.go的新文件,并添加如下行:json
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 pipeline,所以咱们应该进行一些测试。咱们的代码很是简单,只须要一个测试用例便可。确保在点击根URL时收到正确的字符串。在同一目录中建立一个名为main_test.go的新文件,并添加如下行:api
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的构建镜像开始。生成的二进制文件将用于第二个镜像,这只是一个临时镜像。暂存镜像不包含依赖项或库,仅包含启动应用程序的二进制文件。
Service:
因为咱们使用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。
Deployment:
应用程序自己一旦进行了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: "" resources: requests: cpu: 10m
关于此部署定义,最有趣的是镜像部分。咱们不是使用硬编码镜像名称和标签,而是使用一个变量。稍后,咱们将看到如何将该定义用做Ansible的模板,并经过命令行参数替换镜像名称(以及部署的任何其余参数)。
Playbook:
在本实验中,咱们使用Ansible做为部署工具。还有许多其余方式来部署Kubernetes资源,包括Helm Charts,但我认为Ansible是一个更容易的选择。 Ansible使用playbook来组织其说明。咱们的playbook.yml文件以下所示:
- hosts: localhost tasks: - name: Deploy the service k8s: state: present definition: "" validate_certs: no namespace: default - name: Deploy the application k8s: state: present validate_certs: no namespace: default definition: ""
Ansible已经包含用于处理与Kubernetes API服务器通讯的k8s模块。所以,咱们不须要安装kubectl,可是咱们须要一个有效的kubeconfig文件来链接到集群(稍后会详细介绍)。让咱们快速讨论一下该手册的重要部分:
让咱们安装Ansible并使用它自动部署Jenkins服务器和Docker运行时环境。咱们还须要安装openshift Python模块以启用与Kubernetes的Ansible链接。
Ansible的安装很是简单;只需安装Python并使用pip安装Ansible:
安装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
默认状况下,pip将二进制文件安装在用户主文件夹中的隐藏目录下。咱们须要将此目录添加到$PATH变量中,以便咱们能够轻松地调用如下命令:
echo "export PATH=$PATH:~/.local/bin" >> ~/.bashrc && . ~/.bashrc
安装部署Jenkins实例所需的Ansible:
ansible-galaxy install geerlingguy.jenkins
安装docker
ansible-galaxy install geerlingguy.docker
- hosts: localhost become: yes vars: jenkins_hostname: 35.238.224.64 docker_users: - jenkins roles: - role: geerlingguy.jenkins - role: geerlingguy.docker
您须要作的最后一件事是安装如下将在咱们的实验中使用的插件:
如前所述,本实验假设您已经有一个Kubernetes集群启动并正在运行。为了使Jenkins链接到该集群,咱们须要添加必要的kubeconfig文件。在此特定实验中,咱们使用的是托管在Google Cloud上的Kubernetes集群,所以咱们使用的是gcloud命令。您的具体状况可能有所不一样。可是在全部状况下,咱们都必须按照如下步骤将kubeconfig文件复制到Jenkins的用户目录中:
$ sudo cp ~/.kube/config ~jenkins/.kube/ $ sudo chown -R jenkins: ~jenkins/.kube/
请注意,您将在此处使用的账户必须具备建立和管理“部署和服务”的必要权限。
建立一个新的Jenkins做业,而后选择Pipeline类型。做业设置应以下所示:
咱们更改的设置是:
转到 /credentials/store/system/domain/_/newCredentials 并将凭据添加到两个目标。确保为每个都提供有意义的ID和说明,由于稍后会引用它们:
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}\"" } } } } }
该文件比看起来容易。pipeline基本上包含四个阶段:
如今,让咱们讨论这个Jenkinsfile的重要部分:
本文的最后一部分是咱们实际对咱们的工做进行测试的地方。咱们将代码提交到GitHub,并确保咱们的代码在pipeline中移动到达集群:
获取节点的IP地址:
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-46f98c95-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
如今,让咱们向应用程序发起HTTP请求:
$ 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 24a310e] 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..24a310e master -> master
回到Jenkins,咱们能够看到上一次构建失败了:
经过单击失败的做业,咱们能够看到其失败的缘由:
咱们的错误代码将永远不会进入目标环境。