蒼時弦也
蒼時弦也
資深軟體工程師
發表於
這篇文章是 Rails 部署實踐 系列的一部分。

當我們從 Docker Compose 轉換到 Docker Swarm 之後,仍然還是面臨需要人工進行部署操作的狀況,因此我們還需要更近一步的利用 GitLab CI 來幫助我們解決部署的人工操作。

讓 GitLab CI 控制 Docker

GitLab CI 提供了非常多執行的方式,其中一個就是 Docker 模式,也因此我們可以利用這樣的特性來達到在 Docker Swarm 自動部署的效果。

這個技巧來自 Docker Swarm Rocks 網站上的介紹,簡單來說我們在 Manager Node 上以 Docker 的方式啟動 GitLab Runner 並且註冊到 GitLab 上面,因為 GitLab Runner 的 Docker 模式是利用將 /var/run/docker.sock 掛到容器中的方式啟動,自然而然的我們使用 docker 鏡像來執行任務時,自然就等同於對 Manager Node 進行操作。

接下來只需要處理 GitLab Runner 的註冊方式,像是使用標籤(Tag)來限制,或者以 Group Runner 的方式限定在某個團隊下。

在所有或者其中一個 Manager Node 使用以下命令。

1$ docker run -d \
2         --name gitlab-runner \
3         --restart always \
4         -v gitlab-runner:/etc/gitlab-runner \
5         -v /var/run/docker.sock:/var/run/docker.sock \
6         gitlab/gitlab-runner:latest

這樣就啟動了一個「未設定」的 GitLab Runner,接下來我們利用 docker exec 命令進入到容器進行設定。

1$ docker exec -it gitlab-runner bash

進入後,使用 GitLab Runner 的 register 命令進行註冊,這邊可以根據偏好直接填完所有參數,或者透過互動模式一步一步的回答。

1# Interactive Mode 適合節點數較少狀況
2$ gitlab-runner register

要記得在 GitLab 限制這類容器只在有對應的標籤狀況下啟用,以免普通任務佔用到這些節點,造成 Manager Node 的負擔。

GitHub Actions 要採用類似的方案就複雜很多,基於資安上的考量避免將 Docker 的 API 暴露在外網會更好,同時 GitHub Actions 的自訂 Runner 也暫時沒有容器的版本可用,因此無法利用這個技巧,可能就得使用像是 AWS Code Deploy 之類的方式繞一圈來實現。

自動化部署樣板

我們在 Rails 部署實踐 - 撰寫 Docker Compose 曾經提到過 Docker Compose 是可以使用環境變數的,像是利用 .env 來設定,進而避免直接寫在設定檔中,這個特性在 Docker Swarm 也是能夠生效的。

然而,因為命令的設計不太一樣,因此無法讀取到 .env 的設定,必須是使用 export 命令,或者原本存在的環境變數。這個問題在 GitLab CI 中反而不是太麻煩的問題,因此我們可以善用 .gitlab-ci.yml 的設定、GitLab 本身針對不同部署環境套用不同變數的性質,我們可以很彈性的調整。

這可以使用 elct9620/ruby-gitlab-ci 這個專為 Ruby 和 Rails 專案設計的 GitLab 樣板中讓 GitLab 的 Review Apps 能被自動部署的範例作為參考。

 1services:
 2  # ...
 3  application:
 4    image: "${IMAGE_NAME}:${IMAGE_TAG}"
 5    environment:
 6      - AUTO_MIGRATION=yes # Provided by "openbox" gem
 7      - DATABASE_URL=postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB
 8      - RAILS_MASTER_KEY
 9    deploy:
10      placement:
11        constraints:
12        - node.role != manager
13    labels:
14      - traefik.enable=true
15      - traefik.docker.network=traefik-public
16      # Hosts
17      - "traefik.http.routers.${DEPLOY_NAME}-http.rule=Host(`${DEPLOY_DOMAIN}`)"
18      - "traefik.http.routers.${DEPLOY_NAME}-http.entrypoints=web"
19      - "traefik.http.routers.${DEPLOY_NAME}-http.middlewares=https-redirect"
20      - "traefik.http.routers.${DEPLOY_NAME}-https.rule=Host(`${DEPLOY_DOMAIN}`)"
21      - "traefik.http.routers.${DEPLOY_NAME}-https.entrypoints=websecure"
22      - "traefik.http.routers.${DEPLOY_NAME}-https.tls=true"
23      - "traefik.http.routers.${DEPLOY_NAME}-https.tls.certresolver=letsencrypt"
24      - "traefik.http.routers.${DEPLOY_NAME}-https.tls.domains[0].sans=*.${DEPLOY_BASE_DOMAIN}"
25      - "traefik.http.services.${DEPLOY_NAME}.loadbalancer.server.port=3000"
26    networks:
27      - net
28      - traefik-public
29    depends_on:
30      - postgres
31# ...

從上面這段範例可以看出來,裡面大量使用了環境變數。這就表示當我們設計出一個標準的版本後,可以非常容易地套用在不同的專案中,進而快速讓我們把 Rails 專案部署到 Docker Swarm 上。

上述這段範例有不少環境變數都是樣板客製化的,我們可以簡單的來看一下。

像是 IMAGE_NAMEIMAGE_TAG 是從 CI_REGISTRY_IMAGECI_COMMIT_SHORT_SHA 這兩個 GitLab CI 的預先定義環境變數,這樣我們就能夠針對專案、Commit SAH 製作指定的版本,在需要「退版」的時候也可以透過指定版本處理,而且只需要重新執行某次 GitLab CI 的 Pipeline 即可。

至於 DEPLOY_NAME 則是由 CI_PROJECT_IDCI_ENVIRONMENT_SLUG 所組成,這樣就可以產生像是 91-production 的 Docker Stack 命名空間(群組)也能夠很好處理在 GitLab 上有複數專案的情況。

最後我們來看一下 .gitlab-ci.yml 的部署任務該怎麼撰寫。

 1deploy:
 2  image: docker:stable
 3  stage: deploy
 4  environment:
 5    name: production
 6    url: $DEPLOY_DOMAIN
 7  before_script:
 8    - echo "$CI_JOB_TOKEN" | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
 9    - apk -Uuv add curl
10    - curl -LO https://github.com/sudo-bmitch/docker-stack-wait/raw/main/docker-stack-wait.sh
11    - chmod +x docker-stack-wait.sh
12  script:
13    - docker stack deploy -c $DEPLOY_STACK_FILE --with-registry-auth --prune $DEPLOY_NAME
14    - sleep $DEPLOY_WAIT_TIME
15    - ./docker-stack-wait.sh -r $DEPLOY_NAME

首先,我們使用 CI_JOB_TOKEN 來登入 GitLab 的 Registry 如此一來當任務結束時,我們的 Docker Swarm 就會自動的禁止存取容器鏡像,這樣會相對安全,因為能避免被抓取到不想被存取的鏡像。

接下來加入了 docker-stack-wait.sh 這個腳本,主要是因為 docker stack deploy 這個命令實際上是不會「確認部署成功」的,為了讓我們有機會注意到部署失敗,就可以利用這個腳本來幫忙,刻意的「等待部署」來確認是否成功。

另一個原因則是 CI_JOB_TOKEN 也會因為任務結束而失效,如果我們的 Docker Swarm 還在下載要部署的鏡像,即使部署本身沒有問題也會失敗,因此需要確認「部署完畢」才停止部署的任務。


如果想在第一時間收到更新,歡迎訂閱弦而時習之在這系列文章更新時收到通知,如果有希望了解的知識,可以利用Rails 部署實踐回饋表單告訴我。