---
title: "Rails 部署實踐 - 整合 GitLab CI 自動部署"
date: 2022-07-15T00:00:00+08:00
publishDate: 2022-07-15T00:00:00Z
lastmod: 2023-09-03T17:41:34+08:00
tags: ["Rails","教學","部署","實作","Rails 部署實踐","Docker Swarm","GitLab CI","持續部署"]
series: "rails-deployment-in-practice"
toc: true
permalink: "https://blog.aotoki.me/posts/2022/07/15/rails-deployment-in-practice-use-gitlab-ci-with-docker-swarm/"
language: "zh-tw"
---


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

<!--more-->

## 讓 GitLab CI 控制 Docker{#gitlab-ci-control-docker}

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

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

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

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

```bash
$ docker run -d \
         --name gitlab-runner \
         --restart always \
         -v gitlab-runner:/etc/gitlab-runner \
         -v /var/run/docker.sock:/var/run/docker.sock \
         gitlab/gitlab-runner:latest
```

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

```bash
$ docker exec -it gitlab-runner bash
```

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

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

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

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

## 自動化部署樣板{#auto-deploy-template}

我們在 [Rails 部署實踐 - 撰寫 Docker Compose](https://blog.aotoki.me/posts/2022/03/18/rails-deployment-in-practice-write-docker-compose/) 曾經提到過 Docker Compose 是可以使用環境變數的，像是利用 `.env` 來設定，進而避免直接寫在設定檔中，這個特性在 Docker Swarm 也是能夠生效的。

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

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

```yaml
services:
  # ...
  application:
    image: "${IMAGE_NAME}:${IMAGE_TAG}"
    environment:
      - AUTO_MIGRATION=yes # Provided by "openbox" gem
      - DATABASE_URL=postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB
      - RAILS_MASTER_KEY
    deploy:
      placement:
        constraints:
        - node.role != manager
    labels:
      - traefik.enable=true
      - traefik.docker.network=traefik-public
      # Hosts
      - "traefik.http.routers.${DEPLOY_NAME}-http.rule=Host(`${DEPLOY_DOMAIN}`)"
      - "traefik.http.routers.${DEPLOY_NAME}-http.entrypoints=web"
      - "traefik.http.routers.${DEPLOY_NAME}-http.middlewares=https-redirect"
      - "traefik.http.routers.${DEPLOY_NAME}-https.rule=Host(`${DEPLOY_DOMAIN}`)"
      - "traefik.http.routers.${DEPLOY_NAME}-https.entrypoints=websecure"
      - "traefik.http.routers.${DEPLOY_NAME}-https.tls=true"
      - "traefik.http.routers.${DEPLOY_NAME}-https.tls.certresolver=letsencrypt"
      - "traefik.http.routers.${DEPLOY_NAME}-https.tls.domains[0].sans=*.${DEPLOY_BASE_DOMAIN}"
      - "traefik.http.services.${DEPLOY_NAME}.loadbalancer.server.port=3000"
    networks:
      - net
      - traefik-public
    depends_on:
      - postgres
# ...
```

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

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

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

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

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

```yaml
deploy:
  image: docker:stable
  stage: deploy
  environment:
    name: production
    url: $DEPLOY_DOMAIN
  before_script:
    - echo "$CI_JOB_TOKEN" | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
    - apk -Uuv add curl
    - curl -LO https://github.com/sudo-bmitch/docker-stack-wait/raw/main/docker-stack-wait.sh
    - chmod +x docker-stack-wait.sh
  script:
    - docker stack deploy -c $DEPLOY_STACK_FILE --with-registry-auth --prune $DEPLOY_NAME
    - sleep $DEPLOY_WAIT_TIME
    - ./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 還在下載要部署的鏡像，即使部署本身沒有問題也會失敗，因此需要確認「部署完畢」才停止部署的任務。

---

如果想在第一時間收到更新，歡迎[訂閱弦而時習之](https://mailchi.mp/aotoki/rails-deployment-in-practice)在這系列文章更新時收到通知，如果有希望了解的知識，可以利用[Rails 部署實踐回饋表單](https://us4.list-manage.com/survey?u=dd3d68032c0510041f1302539&id=f25e0dc43e&attribution=false)告訴我。

