CI / CD是一个经常与其他术语(例如DevOps,Agile,Scrum和看板,自动化等)一起听到的术语。 有时,它被认为只是工作流的一部分,而没有真正了解它是什么或为什么采用它。 对于年轻的DevOps工程师来说,将CI / CD视为理所当然是很常见的,他们可能还没有看到软件发布周期的“传统”方式,因此无法欣赏CI / CD。
CI / CD代表持续集成/持续交付和/或部署。 未实现CI / CD的团队在创建新软件产品时必须经过以下阶段:
产品经理(代表客户的利益)提供产品应具有的必要功能以及产品应遵循的行为。 该文档必须尽可能详尽和具体。
具有业务分析师的开发人员通过编写代码,运行单元测试并将结果提交到版本控制系统(例如git)来开始处理应用程序。
开发阶段完成后,该项目将移至质量检查。 针对该产品运行了一些测试,例如用户接受测试,集成测试,性能测试。 在此期间,在QA阶段完成之前,不得对代码库进行任何更改。 如果应该有任何错误,则将它们传递给开发人员进行修复,然后将产品交给质量检查人员。
完成质量检查后,操作团队会将代码部署到生产中。
上述工作流程有许多缺点:
首先,从产品经理提出请求到产品准备生产为止,要花费很长时间。
对于开发人员来说,解决一个月或更长时间以来已经编写的代码中的错误非常困难。 请记住,只有在开发阶段结束并且QA阶段开始后才能发现错误。
当紧急代码更改(例如需要修复程序的严重错误)时,由于需要尽快部署,因此QA阶段通常会缩短。
由于不同团队之间几乎没有协作,因此人们会在出现错误时开始指责并互相指责。 每个人开始只关心自己的项目部分,而忽略了共同的目标。
CI / CD通过引入自动化解决了上述问题。 每次将代码更改推送到版本控制系统后,都将进行测试,然后将其进一步部署到登台/ UAT环境中,以进行进一步测试,然后再将其部署到生产环境中供用户使用。 自动化可确保整个过程快速,可靠,可重复且不易出错。
有关此主题的完整书籍已经撰写完毕。 如何,为什么以及何时在您的基础架构中实施它。 但是,只要可能,我们总是更喜欢较少的理论,而是更多的实践。 话虽如此,以下是在提交代码更改后应执行的自动化步骤的简要说明:
持续集成(CI):第一步不包括质量检查。 换句话说,它不关注代码是否提供了客户端请求的功能。 相反,它可以确保代码的质量。 通过单元测试,集成测试,开发人员会很快收到有关代码质量问题的通知。 我们可以通过代码覆盖率和静态分析来进一步扩展测试,从而进一步保证质量。
用户验收测试:这是CD流程的第一部分。 在此阶段,将对代码执行自动测试,以确保其满足客户的期望。 例如,一个Web应用程序可以正常工作而不会引发任何错误,但是客户希望访问者在导航到主页之前,先找到要约的登陆页面。 当前代码将访问者直接带到主页,这与客户的需求有所不同。 UAT测试指出了此类问题。 在非CD环境中,这是人工QA测试人员的工作。
部署:这是CD流程的第二部分。 它涉及对将托管应用程序的服务器/吊舱/容器进行更改,以使其反映更新的版本。 这应该以自动化方式完成,最好通过诸如Ansible,Chef或Puppet之类的配置管理工具来完成。
管道是一个非常简单的概念的幻想。 当您需要以某种顺序执行多个脚本以实现一个共同目标时,这些脚本统称为“管道”。 例如,在詹金斯(Jenkins)中,管道可能包含一个或多个阶段,必须全部完成才能使构建成功。 使用阶段有助于可视化整个过程,了解每个阶段花费了多长时间,并确定构建确切地在哪里失败。
在本实验中,我们正在构建持续交付(CD)管道。 我们正在使用一个用Go编写的非常简单的应用程序。 为了简单起见,我们将仅对代码运行一种类型的测试。 此实验的前提条件如下:
管道可以描述如下:
我们的示例应用程序将以“ 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支持多种将变量注入模板的方法。 在这个特定的实验中,我们使用命令行方法。
让我们安装Ansible并使用它自动部署Jenkins服务器和Docker运行时环境。 我们还需要安装openshift Python模块以启用与Kubernetes的Ansible连接。
Ansible的安装非常简单; 只需安装Python并使用pip安装Ansible:
sudo apt update && sudo apt install -y python3 && sudo apt install -y python3-pip && sudo pip3 install ansible && sudo pip3 install openshift
echo "export PATH=$PATH:~/.local/bin" >> ~/.bashrc && . ~/.bashrc
echo "export PATH=$PATH:~/.local/bin" >> ~/.bashrc && . ~/.bashrc
ansible-galaxy install geerlingguy.jenkins
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
ansible-playbook playbook.yaml.
注意,我们使用实例的公共IP地址作为Jenkins将使用的主机名。 如果使用DNS,则可能需要用实例的DNS名称替换它。 另外,请注意,在运行剧本之前,必须在防火墙(如果有)上启用端口8080。 如前所述,本实验假设您已经有一个Kubernetes
集群运行。 为了使Jenkins连接到该集群,我们
需要添加必要的kubeconfig文件。 在这个特定的实验室中,我们
使用托管在Google Cloud上的Kubernetes集群,因此我们正在使用
gcloud命令。 您的具体情况可能有所不同。 但是总的来说
在这种情况下,我们必须将kubeconfig文件复制到Jenkins的用户目录中
如下:
class = " language-yaml" >$ sudo cp ~ /.kube/ config ~jenkins/.kube/
$ sudo chown class = "token punctuation" >- span > R jenkinsclass = "token punctuation" >: span > ~jenkins/.kube/ code >
请注意,您将在此处使用的帐户必须具有创建和管理“部署和服务”的必要权限。
创建一个新的Jenkins作业,然后选择Pipeline类型。 作业设置应如下所示:
我们更改的设置是:
转到/ credentials / store / system / domain / _ / newCredentials并将凭据添加到两个目标。 确保为每个ID提供有意义的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}\""
}
}
}
}
}
该文件比看起来更容易构建。 管道基本上包含四个阶段:
现在,让我们讨论这个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作为命令行变量传递。 该值将自动替换部署文件中的映像名称。
本文的最后一部分是我们实际对我们的工作进行测试的地方。 我们将代码提交到GitHub,并确保我们的代码在管道中移动直到到达集群:
git add *
git commit -m "Initial commit"
git push
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
$ 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,我们可以看到上一次构建失败了:
通过单击失败的作业,我们可以看到失败的原因:
我们的错误代码将永远不会进入目标环境。
如果您喜欢这些,请在Magalix博客上查看我们的其他教程,注册我们的时事通讯,或立即尝试Magalix。
先前发布在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