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

1.1 Java项目流水线实践

Alt Image Text

  • 使用maven编译打包
  • 使用Sonar扫描
  • 编写Dockerflie构建镜像
  • 自动生成K8s部署文件,替换镜像
  • 使用Kubectl发布部署

  • 代码库simple-java-maven-app

流水线教程(第三节 Jenkins + K8S + Gitlab 构建 RLEASE 打包发布更新流水线到K8S集群)

1.2 NodeJs项目流水线实践

Alt Image Text

  • 初始化前端项目
  • 使用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

Alt Image Text

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

Alt Image Text

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

Alt Image Text

$ 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
                    """

                }
            }
        }

    }   
}

Alt Image Text

Alt Image Text

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

Alt Image Text

Alt Image Text

二、构建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

Alt Image Text

$ 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 中心,我们必须完成许多步骤。

Alt Image Text

https://github.com/itsrakeshhq/productivity-app

首先,我们必须使用命令运行测试以确定所有测试是否通过。如果所有测试都通过,我们将构建 Docker 镜像,然后将这些镜像推送到 Docker Hub。如果您的应用程序极其复杂,您可能需要采取额外的步骤。

现在,假设我们手动完成所有操作,这既费时又可能导致错误。

3.1.2 解决方案

为了解决这个问题,我们可以创建一个 CI/CD流水线。因此,每当您添加功能或修复错误时,都会触发此管道。这会自动执行从测试到部署的所有步骤。

什么是 CI/CD,为什么重要?

持续集成和持续部署是为自动化软件集成和部署而执行的一系列步骤。CI/CD 是 DevOps 的核心

Alt Image Text

从开发到部署,我们的 MERN 应用程序经历了四个主要阶段:测试、构建 Docker 镜像、推送到注册表以及部署到云提供商。所有这些都是通过运行各种命令手动完成的。每次添加新功能或修复错误时,我们都需要这样做。

但这会显着降低开发人员的工作效率,这就是为什么 CI/CD 可以如此有助于自动化这个过程。在本文中,我们将介绍推送到注册表之前的步骤。

3.2 该项目

我们将在本教程中使用的项目是一个非常简单的全栈 MERN 应用程序。

Alt Image Text

它包含两个微服务。

  • 前端
  • 后端

这两个应用程序都包含一个 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。

Alt Image Text

然后创建一个名为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 应用程序。

Alt Image Text

您会注意到,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。

Alt Image Text

3.3 如何编写Jenkinsfile

要创建管道,我们需要一个Jenkinsfile。该文件包含所有管道配置——阶段、步骤等。Jenkinsfile 之于 Jenkins 就像 Dockerfile 之于 Docker。

Jenkinsfile 使用Groovy语法。语法非常简单。看一眼就能明白一切。 让我们开始写:

pipeline {

}

agent 一词应该是您在管道中提到的第一件事。代理类似于运行作业的容器或环境。您可以使用多个代理并行运行作业

pipeline {
 agent any
}

在这里,我们告诉 Jenkins 使用任何可用的代理。

我们的流水线共有 5 个阶段:

Alt Image Text

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 提供访问令牌以访问您的存储库。我建议在此处单击创建访问令牌,因为它是一个具有所有必要权限的模板。然后点击连接。

Alt Image Text

之后,将创建一个管道。由于我们的存储库已经包含一个 Jenkinsfile,Jenkins 会自动检测它并运行我们在管道中提到的阶段和步骤。

如果一切顺利,整个页面将变为绿色。(其他颜色:蓝色表示管道正在运行,红色表示管道出现问题,灰色表示我们停止了管道。)

Alt Image Text

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

现在,提交代码并重新启动管道。

Alt Image Text

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 中使用这些环境变量时所使用的。 添加以下环境变量:

Alt Image Text

然后在 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'
  }
 }
}

再次提交代码并重新启动管道。

Alt Image Text

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'
 }
}

提交代码并重新启动管道。

Alt Image Text

3.5.1 阶段5:将图像推送到注册表

作为最后阶段,我们会将图像推送到 Docker hub。

在此之前,将您的 docker hub 用户名和密码添加到 Jenkins 凭据管理器,但对于种类选择“用户名和密码”。

Alt Image Text

添加我们登录并将图像推送到 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'
  }
 }
}

Alt Image Text

完整的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'
                }
            }
        }
    }
}

Alt Image Text

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

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

Alt Image Text

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 文件

Alt Image Text

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 配置文件的工作目录并安装任何所需的插件。

Alt Image Text

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

Alt Image Text

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

Alt Image Text

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

Alt Image Text

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

Alt Image Text

4.4 测试Jenkins

Jenkins Pipeline 是一套插件,支持在 Jenkins 中实施和集成持续交付管道。

要创建 Jenkins 管道,请在 Web 浏览器中输入“EC2实例的公共IP:8080” 。配置并登录Jenkins后,您应该会看到类似于下图的截图。

Alt Image Text

单击新项目。

  • 输入项目的名称。
  • 选择Pipeline,然后选择Ok。

Alt Image Text

在管道部分,输入以下脚本:

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 仪表板左侧的“立即构建” 。如果您看到绿色的视图,则表示构建已成功完成。

Alt Image Text

4.5 销毁资源

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