最近の砂場活動その15: CI/CDのパイプラインを整備する

最近、新しいPCをセットアップしていたけど、PCのセットアップを完了しないと趣味サイトのdeployすらできないことに気付いた。しばらくdeployしていないと、久しぶりにdeployしたときに大抵事故るし、小まめにやることでdeployの心理的障壁を下げていきたい。現状だと例えば、「寝る前にdeployしてコケると睡眠時間が短かく(2~3時間が飛んでいく)なるから、時間がある週末にやるしかない...」と思ってしまうことが時々ある。そのためにも、テスト済み/ビルド済みの成果物を本番環境にしゅっとリリース可能な状態にしておけるように、CI/CDのパイプラインを整備した。目新しいことは特にはないが、自分用のログを残しておく。

CI/CDの各フェイズの整理

フェイズを分割することで責務を分割して、早い段階でバグに気付いて障害に対応できるようにすることが目的。改めて整理する。

  • Testフェイズ
    • アプリのコードのテストが目的
    • すでにCircleCI上でやっている
  • Buildフェイズ
    • ソースコードや関連するアセットをinputとして、成果物を生成する
    • tarに固めてS3に置いたり、コンテナのイメージにしてECRにpushするなど
      • これまでは手元でDocker buildして、ECRにpushしていた
    • 以下の3つのイメージを準備する必要がある
      • JSONを返すAPI(go)のimage
      • Nuxt.jsでSSR(サーバーサイドレンダリング)するNodeのimage(参考: 最近の砂場活動その11: Nuxt.jsでサーバーサードレンダリング)
      • 静的ファイル(js/css)を配信するNginxのimage
        • 大体はSSRで済むのだが、管理者(私)はアノテーションをぽちぽちやりたい
        • 認証のためAmplifyを使っている && ここだけはクライアントサイドレンダリングでやりたい*1
  • Deployフェイズ
    • Buildフェイズの成果物をAWS ECS(Fargate)上で実行する
    • これまではkayac/ecspressoを使って、手元からdeployしていた
  • Releaseフェイズ
    • Deployフェイズで立ち上げた環境にユーザーへのリクエストを通す

DeployフェイズとReleaseフェイズに分割しているが、自分のサービスの場合、ここがごちゃっと混ざっている。ECSのローリングdeployを選んでいるので、deployされると同時にreleaseされて、新旧それぞれのバージョンのタスクが混在して動いている時間帯がある。障害が発生した場合のロールバックなどは多少時間がかかってしまうが、あくまで趣味プロジェクトなので、大きな影響はない。

新旧タスクの混在を避けたい場合、Blue/Greenデプロイを採用することを考えるとよい。ECSとCodeDeployを連携することで、Blue/Greenデプロイに対応することができる(ECS単体だとダメ)。今回の場合、ひとまず手元からのdeployを止めることが目的なので、ここまではやらないが気が向いたら練習としてやるかもしれない。

Buildフェイズ

GitHub Actions上でbuildして成果物を作って、docker buildしてイメージに含めて、ECRにpushする。

ECRにpushするのに必要なAWS_ECR_ACCESS_KEY_IDAWS_ECR_SECRET_ACCESS_KEYをGitHub ActionsのSecretsとして事前に登録しておく。IAMユーザー(アクセスの種類はプログラムによるアクセスのみを設定)を発行し、最小限の権限を付与するために独自にポリシーを作る。

このフェイズを用意したことによって、Pull Requestがマージされる度に最新のコードが反映されたDocker imageがECRに追加されていく。手元からのbuild & image pushが不要になった。

ビルドに必要なものと最終的なイメージに必要なものは異なることが多い。multi-stage buildを使って、最終的なイメージは最小限のものだけ含まれるようにしておく。

FROM node:alpine as builder
RUN apk add --no-cache git python make g++

WORKDIR /tmp
RUN cd /tmp && git clone https://github.com/syou6162/go-active-learning-web.git
RUN cd /tmp/go-active-learning-web && npm install -g node-gyp && npm install && npm run-script build

FROM nginx:1.19.8
RUN rm -f /etc/nginx/conf.d/*
ADD nginx.conf /etc/nginx/conf.d/app.conf
ADD img /www/app/img
ADD robots.txt /www/app/robots.txt
COPY --from=builder /tmp/go-active-learning-web/.nuxt/dist/client /www/app/_nuxt

CMD ["/usr/sbin/nginx", "-g", "daemon off;", "-c", "/etc/nginx/nginx.conf"]

Deployフェイズ(+ Releaseフェイズ)

sentry

どのエラーがどのリリース以降に出たものかを追跡するため、Sentryを使っている。リリース前にSentryに新しいリリースを登録しておく。

name: Create sentry release

on:
  # masterブランチに対してのみ
  push:
    branches:
      - master

jobs:
  sentry-release:
    runs-on: ubuntu-latest
    env:
      SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
      SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
      SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}        
    steps:
      - uses: actions/checkout@v2
      - name: Create new Sentry release
        run: |
          # Install Sentry CLI
          curl -sL https://sentry.io/get-cli/ | bash
          
          # clone go-active-learning-web
          git clone https://github.com/syou6162/go-active-learning-web.git && cd go-active-learning-web
          
          # Create new Sentry release
          export SENTRY_RELEASE=$(sentry-cli releases propose-version)
          sentry-cli releases new -p $SENTRY_PROJECT $SENTRY_RELEASE
          sentry-cli releases set-commits --auto $SENTRY_RELEASE
          sentry-cli releases finalize $SENTRY_RELEASE
          
          # Create new deploy for this Sentry release
          sentry-cli releases deploys $SENTRY_RELEASE new -e production

ecspresso

ecspressoでDeployを行なう。専用のGitHub Actionsが用意されているので、これもテンプレを埋めていくと割と簡単。

name: Deploy with ecspresso

on:
  # masterブランチに対してのみ
  push:
    branches:
      - master

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: kayac/ecspresso@v0
        with:
          version: v1.1.3
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ECSPRESSO_DEPLOY_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_ECSPRESSO_DEPLOY_SECRET_ACCESS_KEY }}
          aws-region: us-east-1
      - run: |
          ecspresso deploy --config ecs-config.yaml

パイプラインを組む

「Buildフェイズが通った場合のみ、Deployフェイズを実行」など、パイプラインは依存関係を持っていることが多い。GitHub Actionsではneedsに事前に必要なものを指定しておくことで、依存関係を持たせたパイプラインを組むことができる。

workflow_dispatchを指定しておくと、手動Deployをすることもできる。パラメータを指定することもできるので、ロールバックが必要であればtagなどを指定するのもよい。

name: Push nuxt & nginx images to ECR, and deploy

on:
  workflow_dispatch:
  push:

jobs:
  sentry-release:
    runs-on: ubuntu-latest
    env:
      SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
      ...
    steps:
      - uses: actions/checkout@v2
      - name: Create new Sentry release
      ...
  nuxt:
    name: nuxt image push
    runs-on: ubuntu-latest
    needs:
      - sentry-release
    ...
  nginx:
    name: nginx image push
    runs-on: ubuntu-latest
    needs:
      - sentry-release
    ...
  deploy:
    runs-on: ubuntu-latest
    needs:
      - nuxt
      - nginx
    ...

まとめ

元々やりたかった「手元からのDeployをやめる」というのができた。GitHubのPull Requestのマージボタンを押すだけで、buildやdeployまでの自動化ができるようになった。

今回やってないこととしては、以下がある。暇を見て、また砂場活動をやっていこう。

  • DeployフェイズとReleaseフェイズの分離
    • 上に書いてたやつ
  • 環境によるDeployの仕方の変更
    • 例: staging環境はdevelopブランチにマージされたら自動的にdeployまで行なうが、production環境はmainブランチにマージされた段階で初めてdeployする
    • そもそも今はstaging環境は(予算的な意味で)ない...
  • GKE上のDeployの改善

*1:この辺ごちゃついているので、どうにかしたい...