使用Docker和GitLab構建一個CI/CD Pipeline
【編者的話】本文原文連結 ofollow,noindex" target="_blank">here 。本文主要講述瞭如何在GitLab上使用Docker映象構建一個CI/CD的Pipeline。

現如今持續整合(CI)和持續交付(CD)大家已經不陌生了,它們是為了輔助你的產品/工程專案能夠更快、更容易地執行最新版本。在這篇文章中,我將講述如何使用Docker映象和GitLab的CI/CD工具構建一個pipeline,在一個VPS/KVM Linux伺服器上進行部署。
前提要求
- 對Linux、Docker以及CI/CD有基本的瞭解。
- GitLab 帳號(免費計劃即可)。
- 一臺具備SSH訪問許可權的Linux伺服器(非root使用者即可)。我使用的是帶有 LAMP 技術棧的Ubuntu 16.04 LTS系統。
- 裝有SSH和 LFTP 的輕量級Docker映象。
在開始之前,你需要確保:
- 你已經登入GitLab
- 你是某個project/repository的擁有者
- 你能夠在本地機器通過Git訪問這個repo進行pull和push操作
我用的是 GitKraken ,一個Git GUI工具,能夠較為方面的進行Git操作。
關於GitLab的CI/CD
GitLab提供了一種通過Docker和Shared Runners處理CI/CD pipeline的簡單方法。每次執行pipeline時,GitLab都會建立一個獨立的虛擬機器並構建一個Docker映象。Pipeline可以使用YAML配置檔案進行配置,一個pipeline可以有多個job,但如果job太多,pipeline的執行時間就較長。我們肯定不希望這樣,因為使用免費計劃, 每月最多可以有2000分鐘的構建時間 。
“GitLab.com上的 Shared Runners 以 自動縮放模式 執行,由DigitalOcean提供支援。自動縮放意味著減少啟動構建的等待時間,併為每個專案建立隔離虛擬機器,從而最大限度地提高安全性。”
-- 來自GitLab文件中的描述
為GitLab的runner建立SSH金鑰
【備註】:即使你的伺服器上已有具備SSH訪問方式,還是建議你為CI/CD建立一套新的金鑰,同時為部署流程建立一個新的非root使用者。
我們將在Docker容器中通過SSH連線我們的伺服器,這就意味著我們不能輸入使用者密碼(即非互動式登入),因此我們需要在本地計算機中建立 無密碼 的SSH金鑰對。通常我會建立一個 2048位元組的RSA金鑰 ,因為這足夠安全。
$ ssh-keygen rsa -b 2048
輸入以上命令,跟隨建立步驟,如果對建立步驟有疑問,使用 man ssh-key
。記住不要為金鑰對設定密碼。建立完成後,我們需要把私鑰匯入我們的伺服器:
$ ssh-copy-id -i /path/to/key user@host
現在你可以嘗試通過以下命令連線:
$ ssh -i /path/to/key user@host
連線過程應該不會讓你輸入密碼。這個私鑰我們後面會使用到。
選擇Dockerfile
我使用Docker Hub來存放我的定製化Dockerfile,這個Dockerfile將基於 Alpine 構建一個安裝有OpenSSH和LFTP的 輕量級映象(大約8Mb) 。在GitLab的CI/CD中我們需要使用這個映象來執行pipeline的job和指令碼,映象越輕量意味著下載映象的時間就越少。你可以用你自己的映象或者用我的 Dockerfile 。
Pipleline的配置
在正式構建前,你需要在你repo的根目錄建立一個".gitlab-ci.yml"檔案。接下來我將解釋我使用的配置檔案,如果有興趣,你可以先到 GitLab官網 閱讀配置檔案格式以及所有可以使用的配置項。
我的配置檔案如下:
image: jimmyadaro/gitlab-ci-cd:latest Deploy: stage: deploy only: — ‘master’ when: manual allow_failure: false before_script: #Create .ssh directory — mkdir -p ~/.ssh #Save the SSH private key — echo “$SSH_PRIVATE_KEY” > ~/.ssh/id_rsa — chmod 700 ~/.ssh — chmod 600 ~/.ssh/id_rsa — eval $(ssh-agent -s) — ssh-add ~/.ssh/id_rsa script: #Backup everything in /var/www/html/ — ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa $USERNAME@$HOST “zip -q -r /var/backups/www/01-Deploy-$(date +%F_%H-%M-%S).zip /var/www/html/” #Deploy new files to /var/www/html — lftp -d -u $USERNAME, -e ‘set sftp:auto-confirm true; set sftp:connect-program “ssh -a -x -i ~/.ssh/id_rsa”; mirror -Rnev ./ /var/www/html — ignore-time — exclude-glob .git* — exclude .git/; exit’ sftp://$HOST — rm -f ~/.ssh/id_rsa — ‘echo Deploy done: $(date “+%F %H:%M:%S”)’
讓我們逐行看看配置檔案的每一步都在做什麼。
image: jimmyadaro/gitlab-ci-cd:latest
這行將告訴runner從Docker Hub上拉取並執行最新版本的容器。你可以在這裡設定你想要使用的映象,但 別忘了給映象安裝OpenSSH和LFTP 。
Deploy:
這行設定了pipeline的job名字,建立一個job必須設定這行內容。
stage: deploy
這行設定了job的stage名字,如果你需要執行多個stage,例如“backup”、“build”、“deploy”等,stage名字將幫助你識別當前pipeline處於什麼狀態。由於我不需要其他stage,所以我只用了一個job,並且這個job只有一個stage。對於job和stage的名字可以任意設定,例如你的job可以叫“ASDF”,stage可以叫“GHJK”,不過如果你有多個stage,你肯定需要鑑別不同的stage,因此我建議還是規範化這些名字。
only: — ‘master’
這行表示pipeline只有當你repo的 master
分支收到一個更新(例如git merge)時才會被觸發。因此,我建議開發使用其他分支(例如 development
、 wip
等),然後使用 master
分支作為“產品分支”。
when: manual
這行表示你需要進入你的project的CI/CD配置中手動觸發整個部署流程。當然,這一步是可以跳過的,只是我更喜歡手動觸發pipeline。如果去掉這行,你所選分支(本例中為master)的任何改動都會觸發一次pipeline。
allow_failure: false
這行表示如果你的pipeline中有其他stage,當一個job中發生錯誤時,不允許繼續執行剩餘任務。這是一個可選配置。
before_script: #Create .ssh directory — mkdir -p ~/.ssh #Save the SSH private key — echo “$SSH_PRIVATE_KEY” > ~/.ssh/id_rsa — chmod 700 ~/.ssh — chmod 600 ~/.ssh/id_rsa — eval $(ssh-agent -s) — ssh-add ~/.ssh/id_rsa
在 before_script
單元設定的所有命令都會在執行主單元(main script)之前執行。如你所見,每行shell命令需要用短橫線(“-“)指定。上面的命令將把我們剛剛生成的SSH私鑰儲存到容器預設的SSH路徑下,這樣我們就可以免密連線我們的伺服器。
剛剛生成的私鑰將作為Protected變數儲存在我的project的CI/CD配置中,在GitLab的web UI上,點選Settings > CI/CD > Variables將看到這個變數。同樣,我將伺服器地址和部署使用的使用者名稱(非root使用者)也使用Protected變數儲存。

script: #Backup everything in /var/www/html/ — ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa $USERNAME@$HOST “zip -q -r /var/backups/www/01-Deploy-$(date +%F_%H-%M-%S).zip /var/www/html/” #Deploy new files to /var/www/html — lftp -d -u $USERNAME, -e ‘set sftp:auto-confirm true; set sftp:connect-program “ssh -a -x -i ~/.ssh/id_rsa”; mirror -Rnev ./ /var/www/html — ignore-time — exclude-glob .git* — exclude .git/; exit’ sftp://$HOST — rm -f ~/.ssh/id_rsa — ‘echo Deploy done: $(date “+%F %H:%M:%S”)’
script下的內容就是GitLab的runner執行的主單元。首先,我會連線到我的伺服器將所有內容備份到一個ZIP檔案中,這個ZIP檔案將使用當前時間(格式為yyyy-mm-dd_hh-mm-ss)進行命名:
— ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa $USERNAME@$HOST “zip -q -r /var/backups/www/01-Deploy-$(date +%F_%H-%M-%S).zip /var/www/html/”
【備註】:你需要在你的伺服器上安裝ZIP CLI。
在將 /var/www/html
備份後,使用LFTP連線到我的伺服器並且上傳最新的repo檔案。這裡我用的是SFTP,FTP配置有點不一樣:
— lftp -d -u $USERNAME, -e ‘set sftp:auto-confirm true; set sftp:connect-program “ssh -a -x -i ~/.ssh/id_rsa”; mirror -Rnev ./ /var/www/html — ignore-time — exclude-glob .git* — exclude .git/; exit’ sftp://$HOST
使用 mirror -Rnev ./ /var/www/html
讓LFTP上傳 ./
(我repo的根目錄)下的所有檔案到我伺服器的 /var/www/html
路徑下。上面部分引數的意思如下:
-
-u
設定了我們sftp://$HOST
的SSH使用者名稱。 -
-e
用於設定執行命令(使用單引號進行配置)。 -
-R
用於設定reverse mirror。 -
-n
表示只上傳新的檔案。 -
-e
用於刪除在我們源中不存在的檔案。 -
-v
用於配置verbose日誌。 -
ignore-time
將在決定是否下載時忽略時間。 -
exclude-glob .git*
將會排除任何目錄中匹配.git*
的所有檔案(例如.gitignore
以及.gitkeep
)。你可以在這裡設定其他檔案匹配方式。 -
exclude .git/
這個配置將會保證不上傳我們repo中的git檔案。 -
exit
將會停止LFTP和SSH執行。
【備註】:所有在我們服務上但是不在我們repository中的檔案將被刪除,記住上面所述的'源'指的就是我們GitLab的repository。
最終,指令碼會在shared runner的容器中刪除我們的私鑰(這是一個安全措施),並且輸出帶有當前時間的結束語句。
— rm -f ~/.ssh/id_rsa — ‘echo Deploy done: $(date “+%F %H:%M:%S”)’
以上部分就是我配置檔案的所有內容。在GitLab中一個成功的pipeline執行流程如下圖所示:

執行Docker映象

pipeline的最終狀態
結論
我嘗試了一些其他的方式,例如使用rsync替代LFTP、使用多階段以及快取依賴(我能夠重用SSH金鑰)的Jobs、使用Docker的ENTRYPOINT和CMD等等,但我發現上面描述的方式對我來說是最快和最容易的。