一、前端后端项目发布流水线(Java+Nodejs)¶
1.1 Java项目流水线实践¶

- 使用
maven编译打包 - 使用
Sonar扫描 - 编写
Dockerflie构建镜像 - 自动生成
K8s部署文件,替换镜像 -
使用
Kubectl发布部署
流水线教程(第三节 Jenkins + K8S + Gitlab 构建 RLEASE 打包发布更新流水线到K8S集群)
1.2 NodeJs项目流水线实践¶

- 初始化前端项目
- 使用
npm编译打包 - 使用
Sonar前端扫描 - 编写
Dockerfile构建镜像 - 编写K8s部署文件,替换镜像
- 使用
Kubectl发布部署
1.2.1 初始化一个前端项目¶
1、安装 npm 和 vue-cli
- https://chao-xi.github.io/jxjenkinsbook/chap11/2Docker_pipeline/#1-4
install node (wget 下载 然后声明环境变量
// 安装 vue-cli
$ sudo chown -R 1000:1000 "/home/vagrant/.npm"
$ npm install -g @vue/cli-init
2、 gitlab 创建一个新项目:demo-npm-service
demo-npm.yaml
kind: Deployment
apiVersion: apps/v1
metadata:
labels:
k8s-app: npmdemo
name: npmdemo
namespace: demo-uat
spec:
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
k8s-app: npmdemo
template:
metadata:
labels:
k8s-app: npmdemo
namespace: demo-uat
name: npmdemo
spec:
containers:
- name: npmdemo
image: nginx:1.17.7
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: web
protocol: TCP
serviceAccountName: npmdemo
---
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
k8s-app: npmdemo
name: npmdemo
namespace: demo-uat
---
kind: Service
apiVersion: v1
metadata:
labels:
k8s-app: npmdemo
name: npmdemo
namespace: demo-uat
spec:
ports:
- name: web
port: 80
targetPort: 80
selector:
k8s-app: npmdemo

3、快速创建一个 Pipeline 项目 demo_npm_service
#!groovy
@Library('jenkinslib@master') _
def k8s = new org.devops.kubernetes()
def gitlab = new org.devops.gitlab()
def build = new org.devops.buildtools()
pipeline {
agent { node { label "vagrant-agent" }}
parameters {
string(name: 'srcUrl', defaultValue: 'http://192.168.33.1:30088/root/demo-npm-service.git', description: '')
choice(name: 'branchName', choices: 'master\nstage\ndev', description: 'Please chose your branch')
choice(name: 'buildType', choices: 'npm', description: 'build tool')
string(name: 'buildShell', defaultValue: 'install && npm run build', description: 'build tool')
}
stages{
stage('Checkout') {
steps {
script {
checkout([$class: 'GitSCM', branches: [[name: "${branchName}"]], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'gitlab-admin-user', url: "${srcUrl}"]]])
}
}
}
}
}
4、运行这个Pipeline, 然后进入这个项目中初始化项目
$ cd demo_npm_service
$ vue init webpack demo-npm-service

$ cd demo-npm-service
$ npm install && npm run build

$ tree dist/
dist/
├── index.html
└── static
├── css
│ ├── app.30790115300ab27614ce176899523b62.css
│ └── app.30790115300ab27614ce176899523b62.css.map
└── js
├── app.b22ce679862c47a75225.js
├── app.b22ce679862c47a75225.js.map
├── manifest.2ae2e69a05c33dfc65f8.js
├── manifest.2ae2e69a05c33dfc65f8.js.map
├── vendor.936b7041a764ab1c3f2c.js
└── vendor.936b7041a764ab1c3f2c.js.map
3 directories, 9 files
6、传统发布流程
cd dist/ && tar zcf demo-npm-service.tar.gz
cp demo-npm-service.tar.gz /usr/sahre/nginx/html && tar zxf
7、Docker发布流程
Dockerfile
FROM nginx:1.17.7
COPY demo-npm-service/dist/ /usr/share/nginx/html
1.2.2 建立打包发布流水线¶
demo_npm_service
#!groovy
@Library('jenkinslib@master') _
def k8s = new org.devops.kubernetes()
def gitlab = new org.devops.gitlab()
def build = new org.devops.buildtools()
pipeline {
agent { node { label "vagrant-agent" }}
parameters {
string(name: 'srcUrl', defaultValue: 'http://192.168.33.1:30088/root/demo-npm-service.git', description: '')
choice(name: 'branchName', choices: 'master\nstage\ndev', description: 'Please chose your branch')
// choice(name: 'buildType', choices: 'npm', description: 'build tool')
// string(name: 'buildShell', defaultValue: 'install && npm run build', description: 'build tool')
}
stages{
stage('Checkout') {
steps {
script {
checkout([$class: 'GitSCM', branches: [[name: "${branchName}"]], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'gitlab-admin-user', url: "${srcUrl}"]]])
}
}
}
stage("Build&Test"){
steps{
script{
println("执行打包")
sh "cd demo-npm-service && npm install --unsafe-perm=true && npm run build && ls -l dist/"
}
}
}
//构建镜像
stage("BuildImages"){
steps{
script{
println("构建上传镜像")
// env.serviceName = "${JOB_NAME}".split("_")[0]
env.serviceName = "${JOB_NAME}"
withCredentials([usernamePassword(credentialsId: 'docker-registry-admin', passwordVariable: 'password', usernameVariable: 'username')])
{
env.dockerImage = "nyjxi/${serviceName}:${branchName}"
sh """
docker login -u ${username} -p ${password}
docker build -t nyjxi/${serviceName}:${branchName} .
sleep 1
docker push nyjxi/${serviceName}:${branchName}
sleep 1
docker rmi nyjxi/${serviceName}:${branchName}
"""
}
}
}
}
stage('Checkout-For-Master') {
agent { node { label "hostmachine" }}
steps {
script {
checkout([$class: 'GitSCM', branches: [[name: "${branchName}"]], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'gitlab-admin-user', url: "${srcUrl}"]]])
}
}
}
//发布
stage("Deploy"){
agent { node { label "hostmachine" }}
steps{
script{
println("发布应用")
//获取旧镜像
yamlData = readYaml file: "demo-npm.yaml"
println(yamlData[0])
println(yamlData[0]["spec"]["template"]["spec"]["containers"][0]["image"])
oldImage = yamlData[0]["spec"]["template"]["spec"]["containers"][0]["image"]
//替换镜像
sourceData = readFile file: 'demo-npm.yaml'
println(sourceData)
println(sourceData.getClass()) //returns the exact type of an object.
sourceData = sourceData.replace(oldImage,dockerImage)
println(sourceData)
writeFile file: 'demo-npm.yaml', text: """${sourceData}"""
sh """
#cat demo-npm.yaml
kubectl apply -f demo-npm.yaml
"""
}
}
}
}
}
1.2.2 Npm创建与打包¶
stage("Build&Test"){
steps{
script{
println("执行打包")
sh "cd demo-npm-service && npm install --unsafe-perm=true && npm run build && ls -l dist/"
}
}
}
1.2.2 Npm构建镜像¶
stage("BuildImages"){
steps{
script{
println("构建上传镜像")
// env.serviceName = "${JOB_NAME}".split("_")[0]
env.serviceName = "${JOB_NAME}"
withCredentials([usernamePassword(credentialsId: 'docker-registry-admin', passwordVariable: 'password', usernameVariable: 'username')])
{
env.dockerImage = "nyjxi/${serviceName}:${branchName}"
sh """
docker login -u ${username} -p ${password}
docker build -t nyjxi/${serviceName}:${branchName} .
sleep 1
docker push nyjxi/${serviceName}:${branchName}
sleep 1
docker rmi nyjxi/${serviceName}:${branchName}
"""
}
}
}
}
1.2.2 切换构建机器并下载代码(kubectl执行环境所在机器)¶
stage('Checkout-For-Master') {
agent { node { label "hostmachine" }}
steps {
script {
checkout([$class: 'GitSCM', branches: [[name: "${branchName}"]], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'gitlab-admin-user', url: "${srcUrl}"]]])
}
}
}
1.2.2 通过kubectl更新deployment镜像¶
stage("Deploy"){
agent { node { label "hostmachine" }}
steps{
script{
println("发布应用")
//获取旧镜像
yamlData = readYaml file: "demo-npm.yaml"
println(yamlData[0])
println(yamlData[0]["spec"]["template"]["spec"]["containers"][0]["image"])
oldImage = yamlData[0]["spec"]["template"]["spec"]["containers"][0]["image"]
//替换镜像
sourceData = readFile file: 'demo-npm.yaml'
println(sourceData)
println(sourceData.getClass()) //returns the exact type of an object.
sourceData = sourceData.replace(oldImage,dockerImage)
println(sourceData)
writeFile file: 'demo-npm.yaml', text: """${sourceData}"""
sh """
#cat demo-npm.yaml
kubectl apply -f demo-npm.yaml
"""
}
}
}
}
}


Console output
SuccessConsole Output
Started by user admin
Running in Durability level: MAX_SURVIVABILITY
Loading library jenkinslib@master
Examining Chao-Xi/JenkinslibTest
Attempting to resolve master as a branch
Resolved master as branch master at revision 4ff6110f7a787bd8eee5e99e0c93cffd2cd265de
using credential github
> git rev-parse --is-inside-work-tree # timeout=10
Fetching changes from the remote Git repository
> git config remote.origin.url https://github.com/Chao-Xi/JenkinslibTest.git # timeout=10
Fetching without tags
Fetching upstream changes from https://github.com/Chao-Xi/JenkinslibTest.git
> git --version # timeout=10
> git --version # 'git version 2.11.0'
using GIT_ASKPASS to set credentials
> git fetch --no-tags --progress -- https://github.com/Chao-Xi/JenkinslibTest.git +refs/heads/master:refs/remotes/origin/master # timeout=10
Checking out Revision 4ff6110f7a787bd8eee5e99e0c93cffd2cd265de (master)
> git config core.sparsecheckout # timeout=10
> git checkout -f 4ff6110f7a787bd8eee5e99e0c93cffd2cd265de # timeout=10
Commit message: "add kubernetest"
> git rev-list --no-walk 4ff6110f7a787bd8eee5e99e0c93cffd2cd265de # timeout=10
[Pipeline] Start of Pipeline
[Pipeline] node
Running on vagrant-agent in /home/vagrant/workspace/workspace/demo_npm_service
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Checkout)
[Pipeline] script
[Pipeline] {
[Pipeline] checkout
using credential gitlab-admin-user
Fetching changes from the remote Git repository
Checking out Revision f92f92ad70c37f3f09c95bfbb85befde3335fc4e (origin/master)
Commit message: "update dockerfile"
> git rev-parse --is-inside-work-tree # timeout=10
> git config remote.origin.url http://192.168.33.1:30088/root/demo-npm-service.git # timeout=10
Fetching upstream changes from http://192.168.33.1:30088/root/demo-npm-service.git
> git --version # timeout=10
> git --version # 'git version 1.8.3.1'
using GIT_ASKPASS to set credentials
> git fetch --tags --progress http://192.168.33.1:30088/root/demo-npm-service.git +refs/heads/*:refs/remotes/origin/* # timeout=10
> git rev-parse origin/master^{commit} # timeout=10
> git config core.sparsecheckout # timeout=10
> git checkout -f f92f92ad70c37f3f09c95bfbb85befde3335fc4e # timeout=10
> git rev-list --no-walk f92f92ad70c37f3f09c95bfbb85befde3335fc4e # timeout=10
[Pipeline] }
[Pipeline] // script
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Build&Test)
[Pipeline] script
[Pipeline] {
[Pipeline] echo
执行打包
[Pipeline] sh
+ cd demo-npm-service
+ npm install --unsafe-perm=true
npm WARN ajv-keywords@3.5.2 requires a peer of ajv@^6.9.1 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.1.3 (node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.1.3: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.13 (node_modules/watchpack-chokidar2/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.13 (node_modules/webpack-dev-server/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
audited 1274 packages in 8.831s
29 packages are looking for funding
run `npm fund` for details
found 17 vulnerabilities (3 low, 8 moderate, 6 high)
run `npm audit fix` to fix them, or `npm audit` for details
+ npm run build
> demo-npm-service@1.0.0 build /home/vagrant/workspace/workspace/demo_npm_service/demo-npm-service
> node build/build.js
Hash: [1mcc34753b040c844221bc[39m[22m
Version: webpack [1m3.12.0[39m[22m
Time: [1m10232[39m[22mms
[1mAsset[39m[22m [1mSize[39m[22m [1mChunks[39m[22m [1m[39m[22m [1m[39m[22m[1mChunk Names[39m[22m
[1m[32mstatic/js/vendor.936b7041a764ab1c3f2c.js[39m[22m 123 kB [1m0[39m[22m [1m[32m[emitted][39m[22m vendor
[1m[32mstatic/js/app.b22ce679862c47a75225.js[39m[22m 11.6 kB [1m1[39m[22m [1m[32m[emitted][39m[22m app
[1m[32mstatic/js/manifest.2ae2e69a05c33dfc65f8.js[39m[22m 857 bytes [1m2[39m[22m [1m[32m[emitted][39m[22m manifest
[1m[32mstatic/css/app.30790115300ab27614ce176899523b62.css[39m[22m 432 bytes [1m1[39m[22m [1m[32m[emitted][39m[22m app
[1m[32mstatic/css/app.30790115300ab27614ce176899523b62.css.map[39m[22m 797 bytes [1m[39m[22m [1m[32m[emitted][39m[22m
[1m[32mstatic/js/vendor.936b7041a764ab1c3f2c.js.map[39m[22m 619 kB [1m0[39m[22m [1m[32m[emitted][39m[22m vendor
[1m[32mstatic/js/app.b22ce679862c47a75225.js.map[39m[22m 22.2 kB [1m1[39m[22m [1m[32m[emitted][39m[22m app
[1m[32mstatic/js/manifest.2ae2e69a05c33dfc65f8.js.map[39m[22m 4.97 kB [1m2[39m[22m [1m[32m[emitted][39m[22m manifest
[1m[32mindex.html[39m[22m 518 bytes [1m[39m[22m [1m[32m[emitted][39m[22m
Build complete.
Tip: built files are meant to be served over an HTTP server.
Opening index.html over file:// won't work.
+ ls -l dist/
total 4
-rw-rw-r--. 1 vagrant vagrant 518 Aug 29 02:11 index.html
drwxrwxr-x. 4 vagrant vagrant 27 Aug 29 02:11 static
[Pipeline] }
[Pipeline] // script
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (BuildImages)
[Pipeline] script
[Pipeline] {
[Pipeline] echo
构建上传镜像
[Pipeline] withCredentials
Masking supported pattern matches of $username or $password
[Pipeline] {
[Pipeline] sh
+ docker login -u **** -p ****
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
WARNING! Your password will be stored unencrypted in /home/vagrant/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
+ docker build -t ****/demo_npm_service:master .
Sending build context to Docker daemon 162.7MB
Step 1/2 : FROM nginx:1.17.7
---> c7460dfcab50
Step 2/2 : COPY demo-npm-service/dist/ /usr/share/nginx/html
---> 7bfb4e14b75f
Successfully built 7bfb4e14b75f
Successfully tagged ****/demo_npm_service:master
+ sleep 1
+ docker push ****/demo_npm_service:master
The push refers to repository [docker.io/****/demo_npm_service]
33aa390ffe98: Preparing
c26e88311e71: Preparing
17fde96446df: Preparing
556c5fb0d91b: Preparing
17fde96446df: Layer already exists
c26e88311e71: Layer already exists
556c5fb0d91b: Layer already exists
33aa390ffe98: Pushed
master: digest: sha256:b07347f3144a8d72dda47b8b6a8c6688e8be505890d83ec6d6fead467b073982 size: 1158
+ sleep 1
+ docker rmi ****/demo_npm_service:master
Untagged: ****/demo_npm_service:master
Untagged: ****/demo_npm_service@sha256:b07347f3144a8d72dda47b8b6a8c6688e8be505890d83ec6d6fead467b073982
Deleted: sha256:7bfb4e14b75f7844a4d8d5c859e58e18b951e47ea5528cee8cf6092921c7b68c
Deleted: sha256:f8efe6a8a2c3832f337ed137ffbb50db64a2d7e4bbd5844b221acefe5c5e021d
[Pipeline] }
[Pipeline] // withCredentials
...
发布应用
[Pipeline] readYaml
[Pipeline] echo
{kind=Deployment, apiVersion=apps/v1, metadata={labels={k8s-app=npmdemo}, name=npmdemo, namespace=demo-uat}, spec={replicas=1, revisionHistoryLimit=10, selector={matchLabels={k8s-app=npmdemo}}, template={metadata={labels={k8s-app=npmdemo}, namespace=demo-uat, name=npmdemo}, spec={containers=[{name=npmdemo, image=nginx:1.17.7, imagePullPolicy=IfNotPresent, ports=[{containerPort=80, name=web, protocol=TCP}]}], serviceAccountName=npmdemo}}}}
[Pipeline] echo
nginx:1.17.7
[Pipeline] readFile
[Pipeline] echo
kind: Deployment
apiVersion: apps/v1
...
[Pipeline] writeFile
[Pipeline] sh
+ kubectl apply -f demo-npm.yaml
deployment.apps/npmdemo configured
serviceaccount/npmdemo unchanged
service/npmdemo unchanged
$ kubectl get pods -o custom-columns='NAME:metadata.name,IMAGES:spec.containers[*].image' -n demo-uat
NAME IMAGES
npmdemo-5fd79bfd49-gt88v nyjxi/demo_npm_service:master
$ kubectl get svc -n demo-uat
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
npmdemo ClusterIP 10.105.172.158 <none> 80/TCP 6h27m
$ kubectl port-forward svc/npmdemo -n demo-uat 3333:80
Forwarding from 127.0.0.1:3333 -> 80
Forwarding from [::1]:3333 -> 80


二、构建Android项目流水线¶
2.1 配置Android项目开发环境¶
2.1.1 安装JDK¶
下载地址: http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
tar zxf jdk-8u201-linux-x64.tar.gz -C /usr/local
#添加到/etc/profile
export JAVA_HOME=/usr/local/jdk1.8.0_201
export PATH=$PATH:$JAVA_HOME/bin
source /etc/profile
java -version
$ java -version
openjdk version "1.8.0_252"
OpenJDK Runtime Environment (build 1.8.0_252-b09)
OpenJDK 64-Bit Server VM (build 25.252-b09, mixed mode)
2.1.2 安装Android SDK Tools¶
https://developer.android.com/studio/index.html

$ sudo wget wget https://dl.google.com/android/repository/commandlinetools-linux-6609375_latest.zip
$ unzip commandlinetools-linux-6609375_latest.zip
$ sudo cp -r tools/ /opt/tools
$ sudo ln -s /opt/tools/ /usr/local/tools
$ sudo vim /etc/profile.d/sdk.sh
#!/bin/bash
export ANDROID_HOME=/usr/local/
export PATH=$PATH:$ANDROID_HOME/tools/bin
sudo chmod +x /etc/profile.d/sdk.sh
source /etc/profile.d/sdk.sh
2.1.3 SDKmanager¶
https://www.jianshu.com/p/f1f209135d5a
#验证环境变量配置准确
$ sdkmanager --list --sdk_root=/usr/local/tools --no_https --proxy=http --proxy_host=proxy.sha.sap.corp --proxy_port=8080
2.1.4 安装Gradle¶
- https://gradle.org/releases/
- https://chao-xi.github.io/jxjenkinsbook/chap4/1chp4_tools1/#3-gradle
$ gradle -v
------------------------------------------------------------
Gradle 6.5
------------------------------------------------------------
Build time: 2020-06-02 20:46:21 UTC
Revision: a27f41e4ae5e8a41ab9b19f8dd6d86d7b384dad4
Kotlin: 1.3.72
Groovy: 2.5.11
Ant: Apache Ant(TM) version 1.10.7 compiled on September 1 2019
JVM: 1.8.0_252 (Oracle Corporation 25.252-b09)
OS: Linux 3.10.0-957.12.2.el7.x86_64 amd64
$ sdkmanager --list --sdk_root=/usr/local/tools --no_https --proxy=http --proxy_host=proxy.sha.sap.corp --proxy_port=8080
$ sdkmanager "platforms;android-28" --sdk_root=/opt/tools --no_https --proxy=http --proxy_host=proxy.sha.sap.corp --proxy_port=8080
$ sdkmanager "build-tools;26.0.2" "platforms;android-26" --sdk_root=/usr/local/tools --no_https --proxy=http --proxy_host=proxy.sha.sap.corp --proxy_port=8080
sdkmanager "platforms;android-26" --sdk_root=/usr/local/tools --no_https --proxy=http --proxy_host=proxy.sha.sap.corp --proxy_port=8080
$ sdkmanager --licenses --sdk_root=/opt/tools --no_https --proxy=http --proxy_host=proxy.sha.sap.corp --proxy_port=8080
sdkmanager --sdk_root=/usr/local/tools --update && yes | sdkmanager --licenses --sdk_root=/usr/local/tools
$ sdkmanager --uninstall "build-tools;26.0.2" --sdk_root=/opt --no_https --proxy=http --proxy_host=proxy.sha.sap.corp --proxy_port=8080#卸载这个包
三、基于Azure部署Jenkins服务并开发MERN应用的CI/CD构建管道¶
3.1 基于Azure部署Jenkins服务并开发MERN应用的CI/CD构建管道¶
随着开发软件,还必须将其与以前的代码持续集成并将其部署到服务器。手动执行此操作是一个耗时的过程,有时会导致错误。
我们将讨论如何通过使用 Jenkins 设置 CI/CD 管道来改进 MERN(MongoDB、Express、React 和 NodeJs)应用程序开发过程。您将了解如何自动部署以实现更快、更高效的发布。
先决条件
- 对 MERN 堆栈技术的基本了解。
- 对Docker的基本了解。
- 从GitHub获取源代码
3.1.1 问题¶
考虑一下这个生产力应用程序——这是我们将在本文中使用的 MERN 项目。从构建应用程序到将其推送到 Docker 中心,我们必须完成许多步骤。

https://github.com/itsrakeshhq/productivity-app
首先,我们必须使用命令运行测试以确定所有测试是否通过。如果所有测试都通过,我们将构建 Docker 镜像,然后将这些镜像推送到 Docker Hub。如果您的应用程序极其复杂,您可能需要采取额外的步骤。
现在,假设我们手动完成所有操作,这既费时又可能导致错误。
3.1.2 解决方案¶
为了解决这个问题,我们可以创建一个 CI/CD流水线。因此,每当您添加功能或修复错误时,都会触发此管道。这会自动执行从测试到部署的所有步骤。
什么是 CI/CD,为什么重要?
持续集成和持续部署是为自动化软件集成和部署而执行的一系列步骤。CI/CD 是 DevOps 的核心

从开发到部署,我们的 MERN 应用程序经历了四个主要阶段:测试、构建 Docker 镜像、推送到注册表以及部署到云提供商。所有这些都是通过运行各种命令手动完成的。每次添加新功能或修复错误时,我们都需要这样做。
但这会显着降低开发人员的工作效率,这就是为什么 CI/CD 可以如此有助于自动化这个过程。在本文中,我们将介绍推送到注册表之前的步骤。
3.2 该项目¶
我们将在本教程中使用的项目是一个非常简单的全栈 MERN 应用程序。

它包含两个微服务。
- 前端
- 后端
这两个应用程序都包含一个 Dockerfile。
3.2.1 什么是Jenkins?¶
要运行 CI/CD 管道,我们需要一个 CI/CD 服务器。这是管道中编写的所有步骤运行的地方。
市场上有许多可用的服务,包括 GitHub Actions、Travis CI、Circle CI、GitLab CI/CD、AWS CodePipeline、Azure DevOps 和 Google Cloud Build。Jenkins 是一种流行的 CI/CD 工具,我们将在这里使用它。
如何在 Azure 上设置 Jenkins 服务器
因为 Jenkins 是开源的并且它不提供云解决方案,所以我们必须在本地运行它或在云提供商上自行托管。现在,在本地运行可能很困难,尤其是对于 Windows 用户而言。因此,我选择在 Azure 上自行托管此演示。
如果您想在本地运行或在 Azure 以外的地方自行托管(遵循Jenkins 的这些指南),请跳过此部分并继续阅读如何配置 Jenkins部分。
首先,您需要登录您的Azure帐户(如果您还没有,请创建一个)。打开 Azure Cloud Shell。

然后创建一个名为jenkins存储所有 Jenkins 配置的目录,并切换到该目录:
mkdir jenkins
cd jenkins
创建一个名为cloud-init-jenkins.txt. 使用 nano 或 vim 打开,
touch cloud-init-jenkins.txt
nano cloud-init-jenkins.txt
并将此代码粘贴到其中:
#cloud-config
package_upgrade: true
runcmd:
- sudo apt install openjdk-11-jre -y
- wget -qO - https://pkg.jenkins.io/debian-stable/jenkins.io.key | sudo apt-key add -
- sh -c 'echo deb https://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'
- sudo apt-get update && sudo apt-get install jenkins -y
- sudo service jenkins restart
在这里,我们将在创建虚拟机后使用此文件来安装 Jenkins。首先,我们安装 openjdk,这是 Jenkins 运行所必需的。Jenkins 服务会在我们安装后重新启动。
接下来,创建一个资源组。(Azure 中的资源组就像一个容器,将项目的所有相关资源保存在一个组中)
az group create --name jenkins-rg --location centralindia
注意:确保将位置更改为离您最近的位置。
现在,创建一个虚拟机。
az vm create \
--resource-group jenkins-rg \
--name jenkins-vm \
--image UbuntuLTS \
--admin-username "azureuser" \
--generate-ssh-keys \
--public-ip-sku Standard \
--custom-data cloud-init-jenkins.txt
您可以使用以下命令验证 VM 安装:
az vm list -d -o table --query "[?name=='jenkins-vm']"
不要混淆。此命令只是以表格格式显示 JSON 数据,以便于验证。
Jenkins 服务器在 8080 port 上运行,所以我们需要在我们的 VM 上公开这个端口。你可以这样做:
az vm open-port \
--resource-group jenkins-rg \
--name jenkins-vm \
--port 8080 --priority 1010
现在我们可以使用 URL 在浏览器中访问 Jenkins 仪表板http://<your-vm-ip>:8080 。使用此命令获取 VM IP 地址:
az vm show \
--resource-group jenkins-rg \
--name jenkins-vm -d \
--query [publicIps] \
--output tsv
您现在可以在浏览器中看到 Jenkins 应用程序。

您会注意到,Jenkins 要求我们提供一个在安装过程中自动生成的管理员密码。
但首先让我们通过 SSH 进入安装了 Jenkins 的虚拟机。
ssh azureuser@<ip_address>
现在,输入以下命令以获取密码:
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
复制并粘贴它。然后点击继续。
首先,您需要点击Install suggested plugins。安装所有插件需要一些时间。
需要管理员用户来限制对 Jenkins 的访问。因此,继续创建一个。完成后点击保存并继续。
现在您将看到 Jenkins 仪表板。
第一步是安装“Blue Ocean”插件。Jenkins 有一个非常古老的界面,这可能会让一些人难以使用。这个蓝海插件为一些 Jenkins 组件(比如创建管道)提供了一个现代接口。
要安装插件,请转到Manage Jenkins -> 单击System Configuration下的Manage Plugins -> Available plugins。
搜索“Blue Ocean” -> 勾选方框并点击Download now and install after restart。

3.3 如何编写Jenkinsfile¶
要创建管道,我们需要一个Jenkinsfile。该文件包含所有管道配置——阶段、步骤等。Jenkinsfile 之于 Jenkins 就像 Dockerfile 之于 Docker。
Jenkinsfile 使用Groovy语法。语法非常简单。看一眼就能明白一切。 让我们开始写:
pipeline {
}
agent 一词应该是您在管道中提到的第一件事。代理类似于运行作业的容器或环境。您可以使用多个代理并行运行作业
pipeline {
agent any
}
在这里,我们告诉 Jenkins 使用任何可用的代理。
我们的流水线共有 5 个阶段:

3.3.1 第1阶段:下载代码¶
不同的 CI/CD 工具使用不同的命名约定。在 Jenkins 中,这些被称为阶段。在每个阶段,我们编写不同的步骤。
我们的第一个阶段是从源代码管理系统(在我们的例子中是 GitHub)检出代码。
pipeline {
agent any
stages {
stage('Checkout') {
steps {
checkout scm
}
}
}
}
提交更改并推送到您的 GitHub 存储库。
由于我们还没有创建任何管道,现在就开始吧。
在开始之前,我们必须确保 Git 已安装在我们的系统上。如果您按照我之前的步骤在 Azure VM 上安装 Jenkins,则 Git 已经安装。
您可以通过运行以下命令对其进行测试(使您仍然通过 SSH 连接到 VM):
git --version
### install git
sudo apt install git
打开蓝海。单击创建新管道。
然后选择您的源代码管理系统。如果您选择 GitHub,则必须为 Jenkins 提供访问令牌以访问您的存储库。我建议在此处单击创建访问令牌,因为它是一个具有所有必要权限的模板。然后点击连接。

之后,将创建一个管道。由于我们的存储库已经包含一个 Jenkinsfile,Jenkins 会自动检测它并运行我们在管道中提到的阶段和步骤。
如果一切顺利,整个页面将变为绿色。(其他颜色:蓝色表示管道正在运行,红色表示管道出现问题,灰色表示我们停止了管道。)

3.3.2 第2阶段:运行前端测试¶
通常,所有 CI/CD 管道都包含一些需要在部署之前运行的测试。所以我在前端和后端都添加了简单的测试。让我们从前端测试开始。
stage('Client Tests') {
steps {
dir('client') {
sh 'npm install'
sh 'npm test'
}
}
}
我们正在将目录更改为,client/因为那是前端代码所在的位置。然后安装依赖项并在 shell 中npm install运行测试npm test
同样,在我们重新启动管道之前,我们必须确保安装了节点和 npm。在虚拟机中使用这些命令安装节点和 npm:
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt-get install -y nodejs
现在,提交代码并重新启动管道。

3.4 第3阶段:运行后端测试¶
现在对后端测试做同样的事情。
但是在我们继续之前,我们需要做一件事。
如果您看一下代码库activity.test.js,我们会使用一些环境变量。因此,让我们在 Jenkins 中添加这些环境变量。
3.4.1 如何在Jenkins中添加环境变量¶
要添加环境变量,请转到Manage Jenkins -> 单击“Security”下的Manage Credentials -> System -> Global credentials (unrestricted) -> 单击+ Add Credentials。
对于Kind选择“Secret text”,将Scope保留为默认值,对于Secret写入秘密值和ID。这就是我们在 Jenkinsfile 中使用这些环境变量时所使用的。 添加以下环境变量:

然后在 Jenkinsfile 中,使用这些环境变量:
environment {
MONGODB_URI = credentials('mongodb-uri')
TOKEN_KEY = credentials('token-key')
EMAIL = credentials('email')
PASSWORD = credentials('password')
}
添加一个阶段来安装依赖项,在 Jenkins 环境中设置这些变量,然后运行测试:
stage('Server Tests') {
steps {
dir('server') {
sh 'npm install'
sh 'export MONGODB_URI=$MONGODB_URI'
sh 'export TOKEN_KEY=$TOKEN_KEY'
sh 'export EMAIL=$EMAIL'
sh 'export PASSWORD=$PASSWORD'
sh 'npm test'
}
}
}
再次提交代码并重新启动管道。

3.5 第4阶段:构建Docker镜像¶
现在,我们必须指定一个步骤来从 Dockerfiles 构建 Docker 镜像。 在我们继续之前,请在 VM 中安装 Docker(如果您尚未安装它)。 安装 Docker:
sudo apt install docker.io
将用户添加jenkins到docker组中,以便 Jenkins 可以访问 Docker 守护进程——否则您将收到权限被拒绝的错误。
sudo usermod -a -G docker jenkins
然后重启jenkins服务。
sudo systemctl restart jenkins
在 Jenkinsfile 中添加一个阶段。
stage('Build Images') {
steps {
sh 'docker build -t rakeshpotnuru/productivity-app:client-latest client'
sh 'docker build -t rakeshpotnuru/productivity-app:server-latest server'
}
}
提交代码并重新启动管道。

3.5.1 阶段5:将图像推送到注册表¶
作为最后阶段,我们会将图像推送到 Docker hub。
在此之前,将您的 docker hub 用户名和密码添加到 Jenkins 凭据管理器,但对于种类选择“用户名和密码”。

添加我们登录并将图像推送到 Docker hub 的最后阶段。
stage('Push Images to DockerHub') {
steps {
withCredentials([usernamePassword(credentialsId: 'dockerhub', passwordVariable: 'DOCKER_PASSWORD', usernameVariable: 'DOCKER_USERNAME')]) {
sh 'docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD'
sh 'docker push rakeshpotnuru/productivity-app:client-latest'
sh 'docker push rakeshpotnuru/productivity-app:server-latest'
}
}
}

完整的Jenkinsfile
// This is a Jenkinsfile. It is a script that Jenkins will run when a build is triggered.
pipeline {
// Telling Jenkins to run the pipeline on any available agent.
agent any
// Setting environment variables for the build.
environment {
MONGODB_URI = credentials('mongodb-uri')
TOKEN_KEY = credentials('token-key')
EMAIL = credentials('email')
PASSWORD = credentials('password')
}
// This is the pipeline. It is a series of stages that Jenkins will run.
stages {
// This state is telling Jenkins to checkout the source code from the source control management system.
stage('Checkout') {
steps {
checkout scm
}
}
// This stage is telling Jenkins to run the tests in the client directory.
stage('Client Tests') {
steps {
dir('client') {
sh 'npm install'
sh 'npm test'
}
}
}
// This stage is telling Jenkins to run the tests in the server directory.
stage('Server Tests') {
steps {
dir('server') {
sh 'npm install'
sh 'export MONGODB_URI=$MONGODB_URI'
sh 'export TOKEN_KEY=$TOKEN_KEY'
sh 'export EMAIL=$EMAIL'
sh 'export PASSWORD=$PASSWORD'
sh 'npm test'
}
}
}
// This stage is telling Jenkins to build the images for the client and server.
stage('Build Images') {
steps {
sh 'docker build -t rakeshpotnuru/productivity-app:client-latest client'
sh 'docker build -t rakeshpotnuru/productivity-app:server-latest server'
}
}
// This stage is telling Jenkins to push the images to DockerHub.
stage('Push Images to DockerHub') {
steps {
withCredentials([usernamePassword(credentialsId: 'dockerhub', passwordVariable: 'DOCKER_PASSWORD', usernameVariable: 'DOCKER_USERNAME')]) {
sh 'docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD'
sh 'docker push rakeshpotnuru/productivity-app:client-latest'
sh 'docker push rakeshpotnuru/productivity-app:server-latest'
}
}
}
}
}

四、基础设施即代码 - 使用Terraform创建AWS EC2实例并部署Jenkins服务¶
4.1 基础设施即代码 - 使用Terraform创建AWS EC2实例并部署Jenkins服务¶

Terraform 是由 HashiCorp 创建的开源基础设施即代码软件工具。用户使用称为 HashiCorp 配置语言的声明性配置语言(HCL)或可选的 JSON 来定义和提供数据中心基础设施。
您可以使用 Terraform 创建资源,例如 AWS EC2 实例和 AWS S3 存储桶。这些 EC2 实例可以被引导以包含 Jenkins,这是云工程师使用的一种流行的持续集成/持续交付工具。
在此项目中,您将学习如何部署 EC2 实例、引导 EC2 实例以安装和启动 Jenkins、创建 Jenkins 安全组、创建私有 Jenkins S3 存储桶以存储 Jenkins 工件。此外,您还将学习如何创建简单的 Jenkins 管道
4.2 先决条件¶
您需要安装以下工具:
- AWS CLI 安装和配置
- Terraform 安装和配置
- IDE(我使用 VS Code)
4.3 工程初始化¶
在您选择的 IDE 中,创建一个新文件夹,然后cd进入该文件夹。
创建 main.tf、variable.tf、providers.tf 和 outputs.tf 文件

main.tf 将包含主要的配置。
resource "aws_instance" "instance" {
ami = var.ami
instance_type = var.instance
user_data = var.ec2_user_data
vpc_security_group_ids = [aws_security_group.security_group.id]
tags = {
Name = "Jenkins Instance"
}
}
resource "aws_security_group" "security_group" {
vpc_id = var.vpc
ingress {
description = "Allow SSH from my Public IP"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["-.-.-.-/32"]
}
ingress {
description = "Allows Access to the Jenkins Server"
from_port = 8080
to_port = 8080
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "Allows Access to the Jenkins Server"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "Jenkins Security Group"
}
}
resource "aws_s3_bucket" "jojenkinsbucket" {
bucket = "jojenkinsbucket"
}
resource "aws_s3_bucket_acl" "jenkinsbucketacl" {
bucket = aws_s3_bucket.jojenkinsbucket.id
acl = "private"
}
variable.tf 包含变量定义
variable "vpc" {
description = "The Default VPC of EC2"
type = string
default = "vpc-0be40a17d234455e3"
}
variable "ami" {
description = "The AMI ID of the Instance"
type = string
default = "ami-0dfcb1ef8550277af"
}
variable "instance" {
description = "The Instance Type of EC2"
type = string
default = "t2.micro"
}
variable "ec2_user_data" {
description = "User Data for Jenkins EC2"
type = string
default = <<-EOF
#!/bin/bash
sudo yum update -y
sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
sudo yum upgrade
sudo amazon-linux-extras install java-openjdk11 -y
sudo yum install -y jenkins
sudo systemctl enable jenkins
sudo systemctl start jenkins
EOF
}
providers.tf 定义云供应商配置
provider "aws" {
region = "us-east-1"
}
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
outputs.tf代码发布后的输出
output "public_ip" {
value = aws_instance.instance.public_ip
}
4.3.1 基础设施发布¶
terraform init命令将初始化包含 Terraform 配置文件的工作目录并安装任何所需的插件。

terraform validate命令验证目录中的配置文件。

terraform plan命令可让您预览 Terraform 为修改您的基础架构而采取的操作。

terraform apply命令执行 Terraform 计划中建议的操作以创建、更新或销毁基础设施。

此时您可以通过检查控制台来确认 EC2 实例的创建。

4.4 测试Jenkins¶
Jenkins Pipeline 是一套插件,支持在 Jenkins 中实施和集成持续交付管道。
要创建 Jenkins 管道,请在 Web 浏览器中输入“EC2实例的公共IP:8080” 。配置并登录Jenkins后,您应该会看到类似于下图的截图。

单击新项目。
- 输入项目的名称。
- 选择Pipeline,然后选择Ok。

在管道部分,输入以下脚本:
pipeline {
agent any
stages {
stage("build") {
steps {
echo 'Building the application...'
}
}
stage("test") {
steps {
echo 'Testing the application...'
}
}
stage("deploy") {
steps {
echo 'Deploying the application...'
}
}
}
}
选择Jenkins 仪表板左侧的“立即构建” 。如果您看到绿色的视图,则表示构建已成功完成。

4.5 销毁资源¶
如果自己做实验切记删除资源,否则会造成账单消耗。使用terraform destroy销毁资源。