相信有那麼一天,我們將可以像畢凱艦長一樣用嘴巴叫所有主機做事!


Cheng Wei Chen



COSCUP 2017 - Ansible & GitLab CI/CD workshop 101

今年(2017)的 COSCUP 採用與過往不同的徵稿方式,而 DevOps Taiwan 社群也共襄盛舉貢獻了三場分享。

縱然我們知道談到 DevOps 時,不該只談論其中的技術層面,更應該要包含文化層面的議題,不過在這種技術力爆表的場子,就請原諒我們還是會以技術為主要的分享內容。

(不過這麼說有失偏頗,因為 COSCUP 2017 還是有像是「如何將女友培養成開源貢獻者」這種軟性的主題,因此只要有心,還是能夠分享非技術主題的。)

DevOps Taiwan 這次由三位講者,分別負責一場分享,首先我們邀請了部落格「小城故事不多-科技部」的蔡宗城(smalltown)為我們分享「Infrastructure As Code」。

第二位講者則是大家從小看到大的部落格「凍仁的筆記」其主人凍仁翔,為我們分享「DevOps 人一定要知道的 Ansible & GitLab CI 持續交付技巧」。

最後,則由小弟我延續凍仁翔的題目,帶來一場簡易的 Workshop 「Ansible & GitLab CI/CD workshop 101」。

Ansible & GitLab CI/CD workshop 101

這場 Workshop 在構思時,預定的難度是 101 的程度,但實際動手設計內容時發現要講多淺才算是 101?要多複雜才算是 201、301 或更高的難度?有一些難以拿捏,總之就請各位保持著一種開開心心的心情來跟著下面的內容操作練習即可,到底是 101、201、或甚至根本只是幼幼班就先拋在腦後吧(逃)。

回到正題,這場 Workshop 希望能幫助大家藉由實際操作,能夠體驗看看 CI / CD、Automation,以及當我們在談到開發流程的規劃,關於 workflow 或 pipeline 的規劃時可能要注意些什麼?

要注意的是,這個 workshop 的案例只是一個簡單的示範,不同的公司、組織或團隊在規劃 pipeline 時會有各自的考量,因此在實際體驗之後,當你回到自己的團隊時,務必要根據你自身的情況進行規劃,如此才能建構出最適合你團隊的 CI / CD pipeline。

所需工具

本次我們會使用的工具如下:
  • Ansible - 我們會利用 Ansible 撰寫一些簡易的自動化腳本。
  • Docker Community Edition - 用來運行你自己 CI Runner 與模擬 Web Server。
  • 續上,一台可以跑得動 2-3 個 Container 的主機(可以是你自己的筆電、雲端的 VPS,或其他。),預計會消耗 5 GB 以內的硬碟空間,因為在 Workshop 使用的是個人自建又大又肥的 Container。
  • GitLab.com
  • git command line 或其他 git client 工具 - 因為我們會實際把 code 送進 GitLab.com,因此會用到 git 指令,使用你擅長的方式即可。
  • 一台用來輸入指令的電腦(通常就是你現在使用中的 PC 或 Mac。)
接下來我們要先完成幾項事前預備的工作,將依序說明如下。

註冊 GitLab.com 帳號

首先請先註冊一個 GitLab.com 的帳號,各位不用擔心 GitLab.com 宣稱他們靠手上目前的企業客戶足以讓他們永久免費,因此在他們回心轉意之前,註冊與使用皆是免錢的,所以放膽的註冊吧!

下載範例程式碼

請先至下面的 GitLab Project 下載 Workshop 使用的範例程式碼,或者如果你熟悉 PHP Framework Laravel,你也可以自行預備,因為此範例程式碼其實就是乾淨的 Laravel 5.4.*。

https://gitlab.com/DevOpsTaiwan/coscup2017-workshop-101

不過在此 Project 的另一個 branch - demo,最終的成果已放在那裡,因此如果做到一半發現不知道自己哪邊做錯,該 branch 可以作為一個參考。

在你的 GitLab.com 帳號建立一個全新的 Project(Repository)

取得所需的程式碼之後,接著就請於 GitLab.com 上建立一個全新的 Project,並且嘗試將範例程式碼送進該 Project。



預備好你的主機與 Docker Community Edition

如前面「所需工具」說明的,我們需要一台主機,同時該主機要能夠順利運行 Docker Community Edition。

這台主機要用來做什麼呢?即是用來運行你自己專屬的 GitLab CI Runner,雖然 GitLab.com 有提供免費的 CI Runner 可以使用,但如果你有一些自己的顧慮,那麼使用自己的 CI Runner 會比較安心。

另外,因為我們要模擬 deploy 的動作,因此我們也會透過 Docker Container 來模擬假裝我們有好幾台 Server 可以實際進行 deploy 的動作。

以 Docker 運行 GitLab CI Runner

接下來要運行我們自己的 GitLab CI Runner,執行下面的 Docker 指令。

docker run -d --name runner -v /run/docker.sock:/var/run/docker.sock gitlab/gitlab-runner:latest

稍待片刻,等待 Docker 下載 Image 並運行 Container,接著執行另一個指令,以此註冊這個 CI Runner 給 Project 使用。

docker exec -it runner gitlab-runner register

請依據畫面的指示,一一輸入內容,基本上會需要輸入幾項資訊:
  • Please enter the gitlab-ci coordinator URL 請輸入 https://gitlab.com/
  • Please enter the gitlab-ci token for this runner
    這一項可以在 GitLab.com Project -> Settings -> Pipelines -> Specific Runners 中找到。


    (這是我擷取的畫面,請填寫自己的 Token。)
  • Please enter the gitlab-ci description for this runner
    description 可自由輸入,例如: my runner
  • Please enter the gitlab-ci tags for this runner
    這裡請輸入 my-runner
  • Whether to run untagged builds
    維持預設值 false
  • Whether to lock Runner to current project
    如果你的 Runner 在 Workshop 之後,還想留著給其他 Project 使用,可以輸入 false,如果想僅限此 Project 使用,可以輸入true
  • Please enter the executor
    請輸入 docker
  • Please enter the default Docker image
    Runner 預設要使用哪個 Docker image,你可以輸入本次 Workshop 會使用的 chengweisdocker/coscup2017:ci-worker,或其他在你自己的 CI/CD 流程中可能會使用的 Docker image。

若成功註冊 Runner,你重新整理前面查詢 Runner Token 的頁面,即可發現已經將你的 Runner 列在其中。

以 Docker 運行兩台模擬的 Web Server

首先請在主機上取得所需之 Docker image,並直接運行它。

docker run -d -i --name web-server1 chengweisdocker/coscup2017:webserver
docker run -d -i --name web-server2 chengweisdocker/coscup2017:webserver

Container 成功運行之後,接著以手動的方式啟動 Container 內的 SSH 服務。

docker exec web-server1 service ssh start
docker exec web-server2 service ssh start

接著我們要用 Docker 的指令來取得所需的 Container IP。

docker inspect --format '{{ .NetworkSettings.Networks.bridge.IPAddress }}' web-server1

docker inspect --format '{{ .NetworkSettings.Networks.bridge.IPAddress }}' web-server2

這兩個 IP 位置可以留著,後續會使用到它們。

規劃 pipeline

首先我們要大致規劃一下最後的 CI/CD Pipeline 會長成什麼模樣,中間需要經過哪些 Stage,又會執行什麼 Jobs,不論是哪種程式語言,一般 Pipeline 大概脫不了幾個 Stage,如下圖:


但根據你的團隊、專案、程式語言等因素,會有許多差異,因此細節需要根據實際狀況調整。這次 Workshop 規劃的 CI/CD Pipeline 則如下:


這裡先快速提一下每個 Stage 會做哪些動作,之後再來一一詳述操作步驟。
  • build
    這次 Workshop 依然採用平易近人的 PHP Framework - Laravel 作為程式碼的案例,因此在 Stage: Build 我們需要做一些初始安裝的動作,接著就會將程式碼打包產出 Artifacts。
  • unit-test
    CI 的重點之一在於測試,甚至有人說沒有測試的 CI 根本不能發揮「持續整合」的價值,所以這邊我們就先插入一個最基本的 unit test。
  • stg-deploy
    接著當 unit test 通過之後,就自動將程式部署至 staging 測試機上。
  • stg-test
    當然在 staging 測試機上也要再跑一點測試。
  • prod-deploy
    假設 stg-test 也順利通過,接著就可以部署至 production 正式機。

GitLab CI Runner 與 .gitlab-ci.yml

GitLab.com 有提供公共使用的 CI Runner,同時也開放你架設自己的 CI Runner,在本次 Workshop 中,我們兩種都會使用(或者你也可以都使用自己的),因此在前面的「事前預備動作」中,才會提到需要一個可以運行 Docker 的主機,並請各位架設一台 CI Runner。

GitLab CI 使用方便之處在於你只需要在 Project 中新增一個名為 .gitlab-ci.yml 的檔案(也可以在 settings 中設定,改為監看其他的檔名),一旦有程式碼被 commit 至 Project,就會依據 .gitlab-ci.yml 中設定好的內容自動執行 CI 的相關動作。

因此在接下來的操作步驟中,我們會不斷地修改 .gitlab-ci.yml,在其中一一將 Stage 補上,逐漸完成整個 Pipeline。

CI/CD Pipeline - build

如前面所述,第一個 Stage 是 build,我們需要透過 php 的 Dependency Manager - Composer 來幫助我們 install Laravel 所需的 php packages。

首先建立名稱為 .gitlab-ci.yml 的檔案,並且在其中新增以下的內容。

stages:
  - build

build-test:
  stage: build
  tags:
    - "docker"
  image: chengweisdocker/coscup2017:ci-worker
  script:
    - export CI_COMMIT=$(echo $CI_BUILD_REF | cut -b 1-6)
    - touch $CI_PROJECT_NAME-$CI_BUILD_REF_NAME-$CI_COMMIT
    - composer install
    - npm install
    - npm run dev
    - rm -rf node_modules
  artifacts:
    paths:
      - "."
  • stages: 即是描述這個 Pipeline 中有多少 Stage。
  • build-test 是 Job 的名稱,可以隨意命名。
  • tags: 用來設定要以哪些 CI Runner 來執行這個 Job。
  • image: 如果 CI Runner 是採用 Docker 來執行 Job,即可設定 image 告知要用哪一個 Docker Image 來執行 Job,在本次 Workshop 中,基本上我們都會使用 chengweisdocker/coscup2017:ci-worker 這個又肥又大的 Docker image,內含 php、nodejs、ansible,總之本次 Workshop 所需一應俱全。
  • script: 是此 Job 要執行的每一個動作、每一個 Command,想要叫 Runner 幫你執行的動作,就全部填寫於此。
  • artifacts: 則是當 Job 執行完畢之後,要將哪些檔案或資料夾送進 GitLab.com 內建的 Artifacts Repository。而上面輸入的內容意思是,將整個資料夾所有的檔案都送進 Artifacts Repository,如果無法理解沒有關係,繼續做下去就會知道了。

檔案建立完畢之後,就將其 commit 送進 Project 吧。
(git 該如何操作,我就不說明了。)

接著,就可以回到 GitLab.com,找到你的 Project 並進入 Pipeline 查看 CI 執行的結果。


應該可以看到如上圖的狀態,正在 running 當中,當然也有可能已跑完,呈現的是綠色打勾與 passed

若點擊 running 可以進入整個 Pipeline 的畫面,可以發現如下圖,目前這個 Pipeline 只有 一個 Stage 即是 Build。


你可以再更進一步點擊 build-test,即可看到目前 running 的過程。


倘若順利執行完畢,變成 passed 在本頁面的右側即可看到 Job artifacts,即可在此 DownloadBrowse 此 Job 完成的 Artifacts。


接下來我們要在 Stage: Build 中新增第二個 Job。我們將其命名為 build-release,這個 Job 的用途是我們要特別 build 另一個專門給 production 環境使用的 Artifacts。

build-release:
  stage: build
  tags:
    - "docker"
  image: chengweisdocker/coscup2017:ci-worker
  script:
    - export CI_COMMIT=$(echo $CI_BUILD_REF | cut -b 1-6)
    - export ARTIFACT_NAME="$CI_PROJECT_NAME"-"$CI_BUILD_REF_NAME"-"$CI_COMMIT".tar.gz
    - touch $CI_PROJECT_NAME-$CI_BUILD_REF_NAME-$CI_COMMIT

    - composer install --no-dev
    - npm install --production
    - npm run production
    - rm -rf node_modules

    - cd ..
    - tar -zc $CI_PROJECT_NAME/ -f artifacts.tar.gz
    - cp artifacts.tar.gz $CI_PROJECT_NAME/
    - mv artifacts.tar.gz /tmp/$ARTIFACT_NAME
    - ansible-playbook $CI_PROJECT_NAME/ansible/upload_artifacts.yml -e target_host=localhost -e artifact_name=$ARTIFACT_NAME

  artifacts:
    paths:
      - "artifacts.tar.gz"

第二個 Job 與上一個有些類似,但在 script: 有些微的差異。
  • composer install --no-dev 多了 --no-dev 因為一般在開發階段,我們可能會安裝很多輔助開發過程的 packages,例如 php 測試工具之一的 phpunit,但這些 packages 並不需要安裝在 Production 環境中,這時只要妥善的設定 composer.json 並且在 composer install 時,以參數 --no-dev 來區隔即可。

(如圖,composer.json 可以設定哪些 packages 是 dev 才需要安裝的。)
  • npm 這邊也有類似的修改,將原本的 npm install 改成 npm install --production,以及 npm run dev 改成 npm run production。理由與 composer 類似,這邊就不多說明了。
  • 第三個不同之處,在此 Job 中,我們除了一樣將程式碼送進 GitLab 的 Artifacts Repository 之外,我們還要將檔案額外上傳至自己的 Artifacts Repository。因此多了一段利用 tar 將程式碼打包成 .tar.gz 的動作,以及透過 Ansible 來將檔案上傳至指定的位置。
  • 最後一個差異在於,這一次我是將剛才自行用 tar 打包的 artifacts.tar.gz 送進 GitLab 的 Artifacts Repository。

除了修改 .gitlab-ci.yml 之外,我們還要在 Project 中新增 script: 中所使用的 upload_artifacts.yml 這一個 Ansible Playbook。

因此,請在 Project 中新增資料夾 ansible,接著再新增檔案 upload_artifacts.yml

- name: Upload Artifacts
  hosts: "{{ target_host }}"
  gather_facts: no
  become: yes

  tasks:
    - name: Create Folder
      file:
        path: /artifacts/
        state: directory

    - name: Copy Artifact
      copy:
        src: "/tmp/{{ artifact_name }}"
        dest: "/artifacts/{{ artifact_name }}"
        backup: yes
        group: root
        owner: root

讓我們對照一下 script: 中執行的指令。

ansible-playbook $CI_PROJECT_NAME/ansible/upload_artifacts.yml -e target_host=localhost -e artifact_name=$ARTIFACT_NAME

即可知道 Playbook 中所需要的變數 target_hostartifact_name,即是透過 -e target_host=localhost -e artifact_name=$ARTIFACT_NAME 送入 Playbook 之中。

但明眼人應該可以看出來 target_host 我給的值是 localhost,也就是說其實並沒有真正將檔案 Copy 到另一台主機上,只是在 localhost 複製檔案,模擬一下情境而已。倘若你確實有架設另一台主機,那麼你可以實際試試不同的 target_host,將檔案真正上傳至不同的主機上。
上述修改完畢之後,一樣將程式碼送進 Repository,讓 CI 跑跑看是否能夠順利 Passed。


延伸思考:

  • 為何要在 build 時,同步進行兩個 Jobs,一次就將測試與 production 兩種環境的 Artifacts 給建立?
  • 在建立給 production 使用的 Artifacts 時,還有哪些不必要的檔案可以刪除,並不需要一起放進 Artifacts Repository?
  • 為何需要 Artifacts?以及 Artifacts Repository?
  • 將 Ansible Playbook 一起送進同一個 Project 的 Repository 這樣好嗎?又這些自動化腳本應該如何存放?存放於何處?該如何讓 Runner 可以順利取得這些自動化的腳本呢?

CI/CD Pipeline - unit-test

解決了 build,輪到第二個 Stage 是 unit-test。

首先當然是繼續修改 .gitlab-ci.yml,在 stages: 中多增一項。

stages:
  - build
  - unit-test

接著,新增一個 Job,phpunit,然後同樣的 commit,等著看 CI 的結果。

phpunit:
  stage: unit-test
  tags:
    - "docker"
  image: chengweisdocker/coscup2017:ci-worker
  script:
    - composer run-script post-root-package-install
    - composer run-script post-create-project-cmd
    - php vendor/bin/phpunit -c phpunit.xml --coverage-text
  dependencies:
  - build-test

沒意外,CI 的結果應該會如下圖,可以看到 phpunit 執行了哪些測試,以及結果。


這裡額外說明一下為何 script: 中除了 phpunit 的指令之外,又出現了兩個 composer 的指令。這主要是 Laravel 這個 PHP Framework 的設計,它讓開發者可以將 environment 所需要的 env 存放在 .env 這個檔案當中,因此面對不同的 environment,你就可以透過不同的 .env 來管理,例如 staging 與 production 即可有各自的 .env,方便你抽換 environment 的設定。
因此 .env 非常重要,裡面可能會記錄像是 DB 的 User 與 Password 等重要資料,是不可以被送入版本控制系統,而那兩行 composer 的指令即是用來產生出一個 Laravel 所需的 .env 檔案。

(如果不懂 Laravel 沒關係,重點在於要記得,基於不同的專案、程式語言等,管理 environment 或管理所需之 env 的方式很可能會有所不同,如何建立或讓程式能夠取得所需之 env 是建立 Pipeline 必須要仔細考慮的事情之一。)

GitLab 可以設定監看 Job 執行的結果,並自動判斷其中是否有吻合某些特定文字,以此判斷是否要直接在 UI 上顯示測試的結果。


如上圖,回到 Project 的 Setting,在 Pipelne 內,即可找到 Test coverage parsing 的設定,你可以看到已經有 phpunit 的範例,只要照著設定範例設定即可。

設定完畢,往後新執行的 CI Job,都能夠在 PipelinesJobs 中多一欄 Coverage 直接顯示 unit-test 這項 Job 得到的測試覆蓋率數據。


不過這裡要補充說明一下,如果要讓 Coverage 自動顯示,我們還要修改一下 Job 的 script,在 phpunit 那一段要補上一個參數 --colors=never,才能夠順利正常顯示。

    - php vendor/bin/phpunit -c phpunit.xml --coverage-text --colors=never

接著一樣在 Project -> Settings -> Pipelines中找到 Coverage report,並將其中的語法新增至 readme.md


接下來測試覆蓋率的結果就會直接以圖示的方式顯示。

(這部分應該不用多解釋吧? GitLab 與 GitHub 一樣會抓 readme.md 為預設的說明文檔,因此將這些圖示的語法塞進 readme.md,即可讓人在一進入此專案的頁面時,能夠一目了然專案的狀態。)


延伸思考:

  • environment 與所需之 env 應該如何管理為佳?
  • unit test 應該在何時執行?

CI/CD Pipeline - stg-deploy

接下來是我們的重頭戲之一 stg-deploy,在這個 Stage 中我們要將程式碼部署至 staging 環境。有很多團隊都會有 staging 環境,可能是專門提供給測試人員協助測試產品,或提供給外部的客戶測試,或其他的用途。

Deploy 可以是繁瑣動作很多,也可以很簡單,一切就看你如何規劃,當然也會依據專案、程式語言等天性而受到影響,例如假設你的軟體架構很彈性,並吻合 12-factor-app 或 microservices 的架構,那也許你可以將程式碼直接打包成 Docker image,對你來說部署不過就是實踐 Docker 的那句口號「Build, Ship, and Run Any App, Anywhere」。

回到 .gitlab-ci.yml,繼續在其中新贈更多的內容,首先增加一個新的 Stage。

stages:
  - build
  - unit-test
  - stg-deploy

再新增一個 Job。

stg-deploy:
  stage: stg-deploy
  tags:
    - "my-runner"
  image: chengweisdocker/coscup2017:ci-worker
  script:
    - export CI_COMMIT=$(echo $CI_BUILD_REF | cut -b 1-6)
    - mv artifacts.tar.gz /tmp/artifacts.tar.gz
    - ansible-playbook ansible/auto-deploy.yml -i ansible/inventory/demo -e target_host=stg -e host_ip=$stg_host_ip -e user_pass=$stg_user_pass -e CI_PROJECT_NAME=$CI_PROJECT_NAME
  dependencies:
  - build-release

  environment:
    name: staging
    url: http://$stg_domain_name

在此我們終於要使用我們自己架設的 GitLab CI Runner,因此 tags: 要輸入的是 - "my-runner"

而我們的重頭戲 script: 就是那一行 ansible-playbook 的指令,即是透過 Ansible 來替我們完成自動部署至 staging 的動作,因此這裡要新增幾個必要的檔案。

首先是檔名為 auto-deploy.yml 的 Ansible Playbook,一樣就放在 Project 內的 ansible/ 資料夾。

- name: Deploy
  hosts: "{{ target_host }}"
  gather_facts: no
  become: yes

  pre_tasks:
    - set_fact: 
        ansible_host: "{{ host_ip }}"
        ansible_ssh_pass: "{{ user_pass }}"

  tasks:
    - name: Create Folder
      file:
        path: "/deploy/"
        state: directory

    - name: Upload and unarchive artifacts
      unarchive:
        src: "/tmp/artifacts.tar.gz"
        dest: "/deploy/"
        backup: yes
        group: root
        owner: root

    - name: "composer run-script {{ item }}"
      composer:
        command: run-script
        arguments: "{{ item }}"
        working_dir: "/deploy/{{ CI_PROJECT_NAME }}"
      with_items: 
        - post-root-package-install
        - post-create-project-cmd

    - name: Replace nginx site config
      template: 
        src: templates/website.j2
        dest: /etc/nginx/website
        backup: yes
        group: root
        owner: root

    - name: Restagt php7.0-fpm
      service: 
        name: "{{ item }}"
        state: restarted
      with_items: 
        - php7.0-fpm
        - nginx

接著新增資料夾 ansible/inventory/,並在其中新增檔名為 demo 的檔案,檔案的內容如下。

[container:vars]
ansible_port=22
ansible_user=root

[container]
stg 
prod

最後新增資料夾 ansible/templates/,並在其中新增檔名為 website.j2 的檔案,檔案的內容如下。

root /deploy/{{ CI_PROJECT_NAME }}/public;

配合我們在「事前預備」以 Container 模擬的 webserver,我們在 auto-deploy.yml 做了幾件事情。
  • 動態指定目標 Server 的 IP 位置,接著才連上 Server。
  • 建立指定的資料夾。
  • 從 Runner 上傳 Artifacts 至 webserver 並且將 Artifacts 解壓縮至指定的資料夾。
  • 產生 Laravel 所需的 .env 檔案。
  • 設定 Nginx 的 site config。
  • 重新啟動 php7.0-fpm 與 nginx。

這個 Playbook 基本上沒有什麼問題,但只有它還不足以讓我們順利 deploy,讓我們再看一次 scripts: 中設定的動作。

  - ansible-playbook ansible/auto-deploy.yml -i ansible/inventory/demo -e target_host=stg -e host_ip=$stg_host_ip -e user_pass=$stg_user_pass -e CI_COMMIT=$CI_COMMIT

我們設定三個 -e,以及一個 -i
  • -e target_host=stg 這是為了告訴 Playbook 要連上哪一台 Webserver。
  • -e host_ip=$stg_host_ip 用來告知 Playbook,正確的 Staging Webserver 的 IP 位置。
  • -e CI_PROJECT_NAME=$CI_PROJECT_NAME 在此案例中,這是 deploy 的目標資料夾名稱。
  • -i inventory/demo 告知 Playbook 要去哪裡抓取 Staging Webserver 的 SSH 連線資訊。

Inventory 是 Ansible 專門用來紀錄 SSH 連線資訊的檔案,因此我們可以將一些連線資訊放在此檔案中,讓 Playbook 執行時可以根據我們的需要連上指定的 Server。

templates 與 website.j2 則是在設定 Nginx site config 會使用到,因為要讓 Nginx 可以正確地指向 Project 被 deploy 的路徑。

最後我們還要在 GitLab.com 中設定此 Project 的 Secret variables,才能真正確實連上指定的 Server。一樣請進入此 Project -> Settings -> Pipelines 並找到 Secret variables,接著請新增三組 Secret variable。


第一組 Keystg_host_ipValue 請填入「事前預備」中取得的 Container Webserver1 的 IP。

第二組 Keystg_domain_nameValue 同樣請填入「事前預備」中取得的 Container Webserver1 的 IP。

第三組 Keystg_user_passValue 則輸入 coscup2017,這是我事先預備好可以 SSH 登入此 Container 的 User Password。

解釋一下這些動作串連在一起是做了什麼。
  • 首先設定於 GitLab.com 的 Secret variables 會被加密保護,並且在 Runner 執行 Job 時,被送進 Runner 的 env,因此如果你有重要的資訊例如某些 Token 或帳號密碼,不能 commit 在 code 裡面,但是在 Job 執行過程會需要使用到它們,即可設定於 Secret variables。而在此我們則是用來存放 Webserver1 的 IP 位置與 SSH User 的密碼。
  • 在 Playbook 中,我們則是利用 Ansible 的變數,以及pre_taskset_fact 藉此動態的變更 Ansible 要連上的 Server IP 位置與 SSH User 密碼。
  • 於是讓 Runner 執行 Playbook 時,就利用 -e 再把這些 Secret variables 丟給 Playbook 使用。

最後,在這個 CI Job 中還設定 GitLab CI 的另一項功能 environment:

  environment:
    name: staging
    url: http://$stg_domain_name
environment: 可以說是 GitLab 專門為了 deploy 這一種 Job 而推出的功能,只要你設定了 environment:,並且在其中設定好 nameurl,之後只要此 Job 有被成功執行,就會自動彙整在 Project -> Pipelines -> Environments。


如上圖,此頁面方便你可以查看某個 environment 最後一次自動 deploy 是在何時?哪個 branch 或 commit?並且還有一個按鈕讓你可以再次 Re-deploy


延伸思考:

  • deploy 到底需要做哪些動作?
  • 續上 deploy 的腳本該如何設計?Server 又該如何與之配合?
  • 續上,你規劃的自動 deploy 流程可以反覆的 Re-deploy 嗎?
  • inventory、Token 或其他 CI Job 會需要使用的機敏資料應該怎麼管理?並傳遞給 Runner?
  • 如何做到 zero downtime deploy?

CI/CD Pipeline - stg-test

延續我們的情境,當自動部署至 staging 的 Webserver 之後,接著要自動執行 stg-test 的動作,倘若沒有問題最後才能放行執行 prod-deploy。不過在本次的 Workshop 中,我們就跳過這個 Stage,只在 Job 中塞入一些一定會通過的步驟,假裝我們有進行測試。

繼續修改 .gitlab-ci.yml,增加一個新的 Stage。

stages:
  - build
  - unit-test
  - stg-deploy
  - stg-test
再新增一個 Job。
test:
  stage: stg-test
  tags:
    - "my-runner"
  image: chengweisdocker/coscup2017:ci-worker
  script:
    - curl http://$stg_domain_name

這個 Job 很簡單,就是用 curl 去試試看究竟能不能連上 Webserver1 取得網頁資料,如果順利成功,應該會得到 Laravel 預設的入口頁。


延伸思考:

  • staging 環境應該要與 production 環境多相像?
  • 在 staging 環境中應該要執行哪些自動化測試?
  • 除了測試 code 之外,還有哪些東西需要驗證?
  • 如何收集並回報測試的結果?

CI/CD Pipeline - prod-deploy

下一個輪到的 Stage 是 prod-deploy,也就是將程式碼 deploy 至 production 環境。
修改 .gitlab-ci.yml,增加一個新的 Stage。

stages:
  - build
  - unit-test
  - stg-deploy
  - stg-test
  - prod-deploy


再新增一個 Job。

prod-deploy:
  stage: prod-deploy
  tags:
    - "my-runner"
  image: chengweisdocker/coscup2017:ci-worker
  script:
    - export CI_COMMIT=$(echo $CI_BUILD_REF | cut -b 1-6)
    - mv artifacts.tar.gz /tmp/artifacts.tar.gz
    - ansible-playbook ansible/auto-deploy.yml -i ansible/inventory/demo -e target_host=prod -e host_ip=$prod_host_ip -e user_pass=$prod_user_pass -e CI_PROJECT_NAME=$CI_PROJECT_NAME
    - curl http://$prod_domain_name
  dependencies:
  - build-release
  environment:
    name: production
    url: http://$prod_domain_name
  when: manual

此 Job 大部分的動作與 stg-deploy 相同,但仔細看你會發現原本很多 stg_ 的變數,全都改成 prod_

因此請比照辦理,新增 Project 的 Secret variables。
  • prod_host_ip,請依據「事前預備」中得到的 Webserver2 的 IP 位置。
  • prod_domain_name,同上。
  • prod_user_pass,一樣是 coscup2017
以及多了一個 when: manual,這個意思是這項 Job 不要自動執行,而是必須要由人員手動按下 GitLab.com 上的按鈕,才開始執行。


延伸思考:

  • deploy 至 staging 與 production 的差異有哪些?
  • 同一個自動 deploy 的腳本如何才能適用在不同的 environment?
  • 如何做到 zero downtime deploy?
  • 如何驗證 deploy 有確實成功?
  • 如何 rollback?

CI/CD Pipeline - 不同 branch 不同 Pipeline

GitLab CI 可以設定 Job 只有在哪些 branch 時才需要執行,因此針對 .gitlab-ci.yml 還可以再做更多的設定,例如在 stg-deploy、test、prod-deploy 這幾個 Job 都補上 only 這項設定。

stg-deploy:
  stage: stg-deploy
  only:
    - master

... 中略 ...

test:
  stage: stg-test
  only:
    - master

... 中略 ...

prod-deploy:
  stage: prod-deploy
  only:
    - master

... 下略 ...

如此一來,便只有在 master branch 有異動時,才會執行這三個 Job。即可利用不同的 git branch 來設定分別要執行哪些 Job。


延伸思考:

  • 如何管理不同 branch 不同 Pipeline?
  • 哪些 Job 可以共用?

CI/CD Runner - 你的自動化好夥伴

有些人使用 Jenkins 的方式是將其當成一個 Trigger 與 Worker 用它來觸發某些自動化腳本,同樣的 GitLab CI 也可以這樣玩,你可以透過 Trigger 的功能或利用 when: manual 來做到這件事情。

例如你可以新增一個 Stage,叫做 automation

stages:
  - automation

接著就將你一個又一個的自動化腳本。建立為 Job,並且設定要手動觸發。

close-project:
  stage: automation
  script:
    - do something....
  when: manual

於是 GitLab CI Runner 將變成你的好夥伴,只要按下按鈕,它就會自動幫你完成某些事情。


延伸思考:

  • 有哪些動作可以自動化?
  • 注意重複執行的問題。


以上就是這次 Ansible & GitLab CI/CD workshop 101 所有的操作步驟。

可能中間有一些過程沒有在文章中完全詳細說明,這些小細節就容我留在 COSCUP 2017 的現場一一說明。如果你未能實際到場 COSCUP 2017 參加這場 Workshop 101,或者是你在自行練習的過程中有任何的疑問,歡迎留言或前往 DevOps Taiwan 的 FB 社團中與我們一起討論!

或者如果您覺得這個 Workshop 101 可以設計得更棒更好,也歡迎提供您的建議給我們參考,謝謝

4 則留言:

  1. 學到不少,感謝講師,講師讚!

    回覆刪除
    回覆
    1. 感謝彭場!有問題可以在社團中公開提問。

      刪除
  2. 謝謝教學 :) , 新手不是很懂。。
    想請問一下,我在用 build-release建Job時,照著上面教學把指令貼到 .gitlab-ci.yml 出現: Unable to find , 想請問我可能疏漏什麼嗎?
    建build-test我是成功的。 謝謝

    下面是Log 與 錯誤:
    TASK [Create Folder] ***********************************************************
    changed: [localhost]

    TASK [Copy Artifact] ***********************************************************
    fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "msg": "Unable to find '/tmp/artifacts.tar.gz' in expected paths."}
    to retry, use: --limit @/builds/user/DevCosTest/ansible/upload_artifacts.retry

    PLAY RECAP *********************************************************************
    localhost : ok=1 changed=1 unreachable=0 failed=1

    ERROR: Job failed: exit code 1

    回覆刪除
    回覆
    1. 這應該就是我手指太肥的問題,因此文章內貼的 code,跟 repo 上的 code 可能有差異,所以導致你在參考文章操作同時對照 demo 會遇到問題。

      總之關鍵在於你的 .gitlab-ci.yml,以及 upload_artifacts.yml。

      首先說明 upload_artifacts.yml,請檢查
      - name: Copy Artifact
      copy:
      src: "/tmp/artifacts.tar.gz"
      上面這一個 ansible playbook 的 task 中,src 是如何撰寫的。

      接著再檢查 .gitlab-ci.yml 的 build-release,在script 中
      - tar -zc $CI_PROJECT_NAME/ -f artifacts.tar.gz
      - cp artifacts.tar.gz $CI_PROJECT_NAME/
      - mv artifacts.tar.gz /tmp/artifacts.tar.gz
      上面這一段其中的 mv artifacts.tar.gz /tmp/artifacts.tar.gz

      這兩項應該一致才對。

      也就是說,我在 .gitlab-ci.yml 請 Runner 幫我把檔案搬到
      /tmp/ 之下

      然後叫 upload_artifacts.yml 一樣去 /tmp/ 之下取得檔案。
      因此兩邊應該要一致才對。

      文章與 demo 都已經修正,可以再次嘗試看看。

      刪除

不歡迎留言打廣告,所以有進行留言管理,敬請見諒。