Post

CI/CD - Continuous Integration & Continuous Delivery



CI/CD

apply changes everyday without inffect their service

Continuous Integration
  • integrating or merging the code Changes frequently
  • version control
    • at least once per day
  • tools: AWS CodeCommit
Continuous Delivery
  • Automating the build, test and deployment functions
  • tools: CodeBuild. CodeDeploy
Continuous Deployment
  • Fully automated release process
  • code is deployed into Staging or Production as soon as it has successfully passed through the release pipelines
  • tools: CodePipeline

Screen Shot 2020-12-27 at 03.11.51


CI

  • 降低風險。
  • 減少人工手動的繁複程序。
  • 可隨時產生一版可部署的版本。
  • 增加系統透明度。
  • 建立團隊信心。

針對軟體系統每個變動,能持續且自動地進行驗證。此驗證可能包含了:

  • 建置 (build)
  • 測試 (test)
  • 程式碼分析 (source code analysis)
  • 其他相關工作 (自動部署)
  • 驗證完成後,進一步可以整合自動化發佈或部署 (Continuous Delivery / Continuous Deployment) 。

透過此流程可以確保軟體品質 不會因為一個錯誤變動而產生錯誤結果或崩潰(Crash)。 此流程中的各類工具,也會產生一些回饋給開發者或其他角色,包含網頁/報表等等,用來追蹤並改善軟體潛藏的問題。

goal:

  1. software development best practice
  2. make small changes & automate everything
    • small, incremental code changes
    • automate as much as possible
      • slow, error prone, inconsistent, unscalable complex
      • fast, repeatable, scalable, enables rapid deployment
      • test each code change and catch bugs while they are small and simple to fix.
    • code integration, build, test and deployment

CI


CD

CD


AWS Develop Tool

AWS Develop Tool


Other Tools

1
2
3
4
5
6
7
Git — 版本管理
GitHub — 程式碼託管、審查
CircleCI — 自動化建置、測試、部署
Docker — 可攜式、輕量級的執行環境(使用 Docker,統一開發者、測試人員、以及產品的執行環境。)
AWS Elastic Beanstalk — 雲端平台
Slack — 團隊溝通、日誌、通知

sequence-diagram


Action

ref:


Jenkins

  • Jenkins的功能完整,也提供了上千個外掛 (Plugins) 來對應各種開發語言與工具。
  • Jenkins目前已發展到了 2.x 版,新版本中對於 Pipeline 概念及容器 (Container) 整合也趨於完整,是一套可以自訂運用的系統。
  • 但也因為其功能強大、客製程度高,上手需要一些時間。
  • 然而一旦流程被定義,並整合好相關環境,它可以發揮持續性整合威力,大幅增加開發生產力

我們結合了 Git Flow + Protected Branch Flow 的開發流程,流程如下:

  1. 開發者 (Developer) create 一個功能分支 (feature branch)。
  2. 開發者提交一個 Pull Request, SCM 系統會自動觸發 Jenkins 進行建置以及測試。
    1. 這個觸發通常是經由 Webhook 來實現。
  3. 在軟體建置完成後,在 Jenkins 增加一個步驟來送出 Code Scan 的請求給 Sonarqube 系統。
    1. Sonarqube 在完成 Code Scan 後將結果寫回 SCM 系統。
  4. 在 SCM 上連結了 Slack,每個步驟完成(成功或失敗)的通知便可以送到 Slack 群組。
  5. 原碼審核者 (Reviewer) 可以到 SCM 上查看這個Pull Request的相關訊息,搭配 Code Scan的結果決定是否將這個分支合併 (merge) 回主線(develop/master branch)。
  6. 分支合併可以觸發另一個 CI 工作,使 Jenkins 將主線建置後部署到測試環境提供給其他人員進行測試。

在本地端測試 Node.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# ---------------------------- 建立專案資料夾
# 以 hello-ci-workflow 為例:
$ mkdir hello-ci-workflow
$ cd hello-ci-workflow


# ---------------------------- 在本地端執行 Node.js
# 初始化 Node.js 的環境
$ npm init
# 填寫一些資料之後會在目錄下產生一個 package.json 的檔案:
# This utility will walk you through creating a package.json file.
# package name: (hello-ci-workflow)
# version: (1.0.0)
# description:
# entry point: (index.js)
# test command:
# git repository:
# keywords:
# author:
# license: (ISC)
# About to write to /Users/luo/Documents/code/hello-ci-workflow/package.json:
# {
#   "name": "hello-ci-workflow",
#   "version": "1.0.0",
#   "description": "",
#   "main": "index.js",
#   "scripts": {
#     "test": "echo \"Error: no test specified\" && exit 1"
#   },
#   "author": "",
#   "license": "ISC"
# }
# Is this OK? (yes) yes



# 安裝 Node.js 的 web framework,以 Express 為例:
# --save: 寫入 package.json 的 dependencies。
$ npm install express --save



# 完成之後,package.json 大概會長這個樣子:
# add the "scripts" in your package.json
# package.json
{
    "name": "hello-ci-workflow",
    "version": "1.0.0",
    "main": "index.js",
    "scripts": {   # the script it can run
        "test": "echo \"Error: no test specified\" && exit 1",
        "start": "node index.js"
    },
    "author": "",
    "license": "ISC",
    "dependencies": {
        "express": "^4.17.1"
    },
    "devDependencies": {},
    "description": ""
}


# 在 index.js 裡寫一段簡單的 Hello World! 的程式:
# This app starts a server and listens on port 3000 for connections. The app responds with “Hello World!” for requests to the root URL (/) or route. For every other path, it will respond with a 404 Not Found.
# index.js:
var express = require('express');
var app = express();
const port = 3000

app.get('/', function(req, res){
  res.send('Hello World!');
});

var server = app.listen(port, function(){
    var host = server.address().address;
    var port = server.address().port;
    console.log("Example app listening at https://%s:%s", host, port);
});



# 執行 npm start 或 node index.js:
$ npm start


# 打開瀏覽器 https://localhost:3000 看結果:

測試 Node.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 安裝 Node.js 的單元測試,以 Mocha 為例:
$ npm install mocha --save-dev
# --save-dev: 寫入 package.json 的 devDependencies,正式上線環境不會被安裝。
# package.json
{
    "name": "hello-ci-workflow",
    "version": "1.0.0",
    "main": "index.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "start": "node index.js"
    },
    "author": "",
    "license": "ISC",
    "dependencies": {
        "express": "^4.17.1"
    },
    "devDependencies": {
        "mocha": "^8.2.1"   # added
    },
    "description": ""
}



# 根目錄 test 資料夾,並新增一個測試腳本 test.js:
$ mkdir test
$ cd test
$ touch test.js



# 加入一筆錯誤的測試 assert.equal(1, [1,2,3].indexOf(0)):
# test/test.js
var assert = require("assert")

describe('Array', function(){
  describe(' #indexOf()', function(){
    it('should return -1 when the value is not present', function(){
      assert.equal(1, [1,2,3].indexOf(0));
      # return true, 0
    })
  })
})



# 執行 mocha 測試:
$ ./node_modules/.bin/mocha
# 結果顯示 1 failing,測試沒通過,因為 [1,2,3].indexOf(0) 回傳的值不等於 -1。
  Array
    #indexOf()
      1) should return -1 when the value is not present
  0 passing (9ms)
  1 failing


# 將 test.js 的測試修正:
# test/test.js
assert.equal(-1, [1,2,3].indexOf(0)); # return false, -1


# 再次執行 mocha 測試:
$ ./node_modules/.bin/mocha
# 結果顯示 1 passing,通過測試。
  Array
    #indexOf()
      ✓ should return -1 when the value is not present
  1 passing (6ms)

CircleCI

1. upload code to GitHub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# ---------------- 初始化 git 環境:
$ git init .
# Initialized empty Git repository in /Users/luo/Documents/code/hello-ci-workflow/.git/


# ---------------- 顯示目前哪些檔案有過更動:
$ git status
# On branch master
# Initial commit
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#   index.js
#   node_modules/
#   package.json
#   test/


# ---------------- .gitignore
# 將 node_modules 加到 .gitignore 黑名單
# 這個資料夾是由 npm install 自動產生的,不需要放到 GitHub 上:
vim .gitignore
# .gitignore
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
*.exe       # 忽略所有 xxx.exe 的檔案
Builds*
*.debug



# ---------------- 更動 commit:
$ git add .
$ git commit -m "first commit"


# ---------------- repository:
# 打開 GitHub,新增一個 repository:
# 輸入 repository 的名稱,以 hello-ci-workflow 為例:
# 使用 git remote add 將新創建的 GitHub repository 加入到 remote:
$ git remote add origin https://github.com/ocholuo/hello.git
$ git remote set-url origin https://github.com/ocholuo/hello.git
$ git show-ref
a9f90c6c817514c9484de1df9f317f2840d8b24c refs/heads/main


# ---------------- 將程式碼傳到 GitHub:
$ git push -u origin origin/main
$ git push -u origin master

# 成功之後前往 https://github.com/<USER_NAME>/hello-ci-workflow 就可以看到剛才上傳的檔案:

2. setup CircleCI 加入 GitHub repository

1
2
3
# 搜尋要加入的 GitHub repository,然後點選 Build project 按鈕,以 hello-ci-workflow 為例:
# 完成之後 CircleCI 就會自動執行第一次的建構
# 因為還沒加入測試腳本,所以建構結果會顯示 no test:

3. 在 CircleCI 測試 Node.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# ---------------- 在根目錄建立 circle.yml, 加入 mocha test:
# circle.yml
version: 2.1
orbs:
  node: circleci/node@4.1.0
jobs:
  test:
    executor:
      name: node/default
      tag: '13.14'
    steps:
      - checkout
      - node/install-packages
      - run:
          # command: npm run test
          # command: npm start
          command: ./node_modules/.bin/mocha
workflows:
  test_my_app:
    jobs:
      - test



# # ---------------- push 上 GitHub:
$ git add circle.yml
$ git commit -m "add circle.yml"
$ git push


# Push 成功之後,CircleCI 會自動觸發建構和測試:

# 測試通過,建置成功:

4. Code Review with GitHub Flow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 建立一條分支
# 為了確保 master 這條主線上的程式碼都是穩定的,
# 所以建議開發者依照不同的功能、建立不同的分支,
# 這裡以 test-branch 為例


# ----------------  git branch 新增+切換分支:
$ git branch test-branch
$ git checkout test-branch


# ---------------- 在 test.js 裡加入一行錯誤的測試 assert.equal(3, [1,2,3].indexOf(5)):
assert.equal(3, [1,2,3].indexOf(5));


# ---------------- 加入 commits
$ git add test/wtest.js
$ git commit -m "add a error test case"


# ---------------- 新增一個 Pull Request
# Push 到 GitHub 的 test-branch 分支:
$ git push -u origin test-branch


# 打開 GitHub
# 出現 test-branch 分支的 push commits,點選旁邊的 Compare & pull request 按鈕:





# ---------------- 進入 Open a pull request 的填寫頁面
# 選擇想要 merge 的分支、輸入描述之後
# 點選 Create pull request 按鈕:
# 新增一個 pull request 之後,其他人就會在 GitHub 上出現通知:
# 點進去之後可以看見相關的 commits 與留言,但是下面有一個紅紅大大的叉叉;因為每次 GitHub 只要有新的 push,就會觸發 CircleCI 的自動建置和測試,並且顯示結果在 GitHub 上:


# 點選叉叉,前往 CircleCI 查看錯誤原因:


# 就會發現剛剛 push 到 test-branch 的測試沒通過:


# 回到 GitHub,因為測試沒通過,所以審查者不能讓這筆 pull request 被 merge 回 master。


# 找到剛剛 commit 的那段程式碼,留言告知請開發者修正錯誤之後,再重新 commit push 上來:


# 修正 test.js 的測試腳本:
assert.equal(-1, [1,2,3].indexOf(5));



# 再次 commit & push:
$ git add test/test.js
$ git commit -m "fix error test case"
$ git push


# 回到 GitHub 的 pull request 頁面,可以看到最新一筆的 commit 成功通過 CircleCI 的測試了:

5. Merge&部署

1
2
3
# 審查之後,確定沒有問題
# 點選 Merge pull request 的按鈕
# 將 test-branch 的程式碼 merge 回主線 master:

Docker

以往開發人員面對開發環境不同的問題,常常出現「明明在我的電腦上可以跑」的囧境,所以為了解決這類問題,通常會使用虛擬機器(VM)搭配一些工具(Vagrant、Chef)來協助統一開發人員、測試人員、上線產品的執行環境。

vm-vs-docker

Docker 也是類似的解決方案,不同於 VM 的是,Docker 運行起來更輕巧、可攜度更高。配置好一份設定之後,就可以讓大家馬上進入開發狀況,減少不必要的環境問題,提升效率。

different

  1. setup a docker run
  2. use CircleCI to run on a docker (no need to build)

在本地测试 Docker 上的 Node.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 在專案根目錄底下建立一個 Dockerfile:
# Dockerfile

# 從 [Docker Hub](https://hub.docker.com/) 安裝 Node.js image。
FROM node:0.10
# 設定 container 的預設目錄位置
WORKDIR /hello-ci-workflow
# 將專案根目錄的檔案加入至 container
# 安裝 npm package
ADD . /hello-ci-workflow
RUN npm install
# 開放 container 的 3000 port
EXPOSE 3000
CMD npm start


FROM node:0.10
WORKDIR /hello-ci-workflow
ADD . /hello-ci-workflow
RUN npm install
EXPOSE 3000
CMD npm start


# 使用 docker build 建構 image:
# -t image _name。
$ docker build -t hello-ci-workflow .
# Sending build context to Docker daemon  8.998MB
# Step 1/6 : FROM node:0.10
# 0.10: Pulling from library/node
# 386a066cd84a: Pull complete
# 0adf07c73141: Download complete



# 使用 docker run 執行您的 image:
# -d 在背景執行 node,可以使用 docker logs 看執行結果。
# 打開瀏覽器 https://localhost:3000 看結果:
$ docker run -p 3000:3000 -d hello-ci-workflow


# 其實每一次都要 build 和 run 還蠻麻煩的,推薦可以試試 Docker Compose,用起來有點像 Vagrant。

在 CircleCI 測試 Docker 上的 Node.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# ---------------- 在根目錄建立 circle.yml
# circle.yml
machine:
  # 環境改成 docker
  services:
    - docker

dependencies:
  override:
    # 建構方式使用 docker build
    - docker build -t hello-ci-workflow .

test:
  override:
    - ./node_modules/.bin/mocha
    # 使用 curl 測試 docker 是否有順利執行 node
    - docker run -d -p 3000:3000 hello-ci-workflow; sleep 10
    - curl --retry 10 --retry-delay 5 -v https://localhost:3000


# Push 更新到 GitHub:
$ git add Dockerfile circle.yml
$ git commit -m "add Docker"
$ git push

AWS Elastic Beanstalk

AWS Elastic Beanstalk

  • 只需要上傳程式碼,Elastic Beanstalk 即可幫你完成
  • 容量配置、負載均衡(load balancing)、自動擴展(auto scaling), 應用程式的運行狀況監控的部署。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 初始化 EB 環境:
$ eb init -p docker

# 該命令將提示配置各種設置。 按 Enter 鍵接受預設值。
# 已經存有一組 AWS EB 權限的憑證,該命令會自動使用它。
# 否則輸入 Access key ID 和 Secret access key,必須前往 AWS IAM 建立一組。


# 初始化成功之後,可以使用 eb create 快速建立各種不同的環境,例如:development, staging, production。
# 以 env-development 為例, 當它完成之後,您的應用已經備有負載均衡(load-balancing)與自動擴展(autoscaling)的功能了。
$ eb create env-development


# 使用 eb open 前往目前版本的執行結果:
$ eb open env-development

在本地端部署 AWS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 稍微修改 index.js:

# index.js
# ...
app.get('/', function (req, res) {
  res.send('Hello env-development!');
});
# ...


# 執行 eb deploy 部署新版本到 AWS Elastic Beanstalk:
$ eb deploy env-development

# 部署完成之後,執行 eb open 打開網頁:
$ eb open env-development

在 CircleCI 部署 AWS

1
2
3
4
5
6
# git checkout 將分支切換回主線 master:
$ git checkout master

# eb create 新增一組新的環境,作為產品上線用,命名為 env-production:
$ eb create env-production
$ eb open env-production

Slack


This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.