百番煎じだけど、チームの Github Actions 活用方法とデプロイの流れを紹介してみる

はじめに

この記事は BBSakura Networks Advent Calendar 2023 の 21 日目の記事です。 adventar.org

こんにちは、BBSakura Networks のシステム管理部に所属している蟹江(@kanix2929)です。普段は BBIX から委託されているシステムなどの開発・運用がメインで、ネットワークエンジニア / オペレーターを手助けするためのシステム構築・自動化に尽力しています。

最近、複数人で同時並行的に 1 つのコードを触る場合が増えてきたことから、あらためてチーム内で開発プロセス(GitHub の運用方法など)についてまとめたので紹介してみます。

環境について

  • 本番環境の構成
    • VM 上で Docker コンテナを起動させている
      • docker-compose.yml でコンテナ管理している
    • コンテナが動いているサーバーはプロキシ配下
  • ステージング環境の構成
    • 本番環境を模している(データだけ違う)
  • CI/CD ツールとして Github Actions を主に使っている
    • コード管理に GitHub を使用しているので運用が楽

開発の流れとルール

ざっと絵にするとこんな感じ

開発者がプルリクを出すことでGitHub Actionsが自動的にステージング用と本番環境用イメージを作成して開発していく様子を描いています
GitHub Actionsを使用した開発フローの図

流れを言語化すると

  1. コードを修正してプッシュし、main ブランチへのプルリクを作成
    • プルリク作成時にブランチ名のタグを打った Docker イメージが自動生成され、Docker Hub にプッシュされる(build-image.yml
    • 後述の Release Please のためにプルリクのタイトルは Conventional Commits に従う必要あり
      • タイトルをチェックするワークフローも作成済み(lint-PR-title.yml
  2. プルリク作成者が自身でステージング環境にて動作確認
    • ステージングのコンテナを入れ替えるときは Slack で一声かける
  3. 自身での動作確認後、チームメンバーにレビュー & 動作確認を依頼する
  4. レビュアーは該当プルリクが問題ないことを確認して、プルリクをマージする
    • 基本的にはレビュアーがマージする
    • プルリクのマージは Squash and merge
    • リポジトリの設定でも制限
  5. マージすると main タグの Docker イメージが生成される(build-image.yml
  6. マージ時に Release Please のワークフローも動いて Bot がプルリクを生成するので確認してマージする
    • GitHub 上で Semantic Versioning に従ってタグが打たれる
    • 上記 Version をタグ名とした Docker イメージが自動生成され、Docker Hub にプッシュされる(build-image.yml
  7. 本番へリリース
    • コンテナを立ち上げるのは基本人手
    • デプロイを自動化しても良いが、結局確認が必要なので
      • みんなが使っているステージング環境において、どのタイミングでコンテナ立ち上げ直していいのかちょっと決めづらいのでそちらも人手

もろもろの採用理由

  • Squash and merge の理由
    • 開発していくときのコミットがある程度適当でも他人に影響が無い
      • 開発する人が開発しやすい粒度でコミットしたい
      • Create a merge commit だと、コミット粒度についてチームで統一しておかないと逆に見づらくなると思っている
      • もしバグ含んだプルリクをマージしてしまった場合は、該当プルリクをまるっと Revert するか Fix のコミットを当てる
    • 「1 機能 1 プルリク」にすることで、「1 機能 1 コミット」となるので、コミット履歴がスッキリして見やすい
    • プルリクのタイトルだけ見れば良いので、release-please.yml でのバージョン管理が楽
  • プルリク作成時にイメージを作成する理由
    • 各人がステージング環境での動作確認をしやすくするため

おわりに

GitHub Actions をメインとした CI/CD についての紹介は今さら感ありますが、まだ GitHub の運用がざっくりしているチームの方たちの参考になればなあ、と思っています。 今回紹介した方法が完成形というわけではないので、これからも柔軟に運用をアップデートしていく予定です。 また、みなさんの開発体制で「もっと良い方法があるよ」ということがあれば、ぜひ教えていただきたいです!

[参考]

参考として yml ファイルをいくつか貼っておきます。

build-image.yml

単純に Docker イメージを生成するための yml ファイルです。

name: build-and-push-to-dockerhub

env:
  IMAGE_NAME: hogehoge

on:
  workflow_dispatch:
  push:
    branches:
      - "main"
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+*'
  pull_request:
    branches:
      - "main"

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Set TAG
        run: |
          if [[ "${{ github.event_name }}" == "pull_request" ]]; then
            TAG=$(echo "${{ github.head_ref }}" | tr '/' '-')
          else
            TAG=$(echo "${{ github.ref_name }}" | tr '/' '-')
          fi
          echo "TAG=${TAG}" >> $GITHUB_ENV
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${IMAGE_NAME}
      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: fugafuga
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: "${{ env.IMAGE_NAME }}:${{ env.TAG }}"
          labels: ${{ steps.meta.outputs.labels }}

ちなみに

次のように、本番環境とステージング環境でイメージビルド時に使う変数だけ変更したい場合には、スクリプトで無理やり埋め込んだりしています。

  • Docker でマルチステージビルドを使っているのでイメージのビルドとコンテナの起動のステージが違う
  • NEXT_PUBLIC_* のようなビルド時に展開される変数を使っている
    • 本番環境とステージングで別の変数を使いたい

参考として yml はここに折りたたんでおきます。

タグごとに変数を指定する Docker イメージを生成するための yml ファイルです。

name: build-image-and-push-to-dockerhub

env:
  IMAGE_NAME: hogehoge

on:
  workflow_dispatch:
  push:
    branches:
      - main
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+*'
  pull_request:
    branches:
      - main

jobs:
  build-image:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repo
        uses: actions/checkout@v4
      - name: Set TAG
        run: |
          if [[ "${{ github.event_name }}" == "pull_request" ]]; then
            TAG=$(echo "${{ github.head_ref }}" | tr '/' '-')
          else
            TAG=$(echo "${{ github.ref_name }}" | tr '/' '-')
          fi
          echo "TAG=${TAG}" >> $GITHUB_ENV
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${IMAGE_NAME}
      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username:  fugafuga
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      # 以下で変数を設定している
      - name: SET ENV for Dockerfile
        run: |
          if [[ "${{ github.event_name }}" == "pull_request" ]]; then
            TAG=$(echo "${{ github.head_ref }}" | tr '/' '-')
            NEXT_PUBLIC_EXAMPLE_ENV=http://example-staging.com
          else
            TAG=$(echo "${{ github.ref_name }}" | tr '/' '-')
            NEXT_PUBLIC_EXAMPLE_ENV=http://example.com
          fi
          echo "TAG=${TAG}" >> $GITHUB_ENV
          echo "NEXT_PUBLIC_EXAMPLE_ENV=${NEXT_PUBLIC_EXAMPLE_ENV}" >> $GITHUB_ENV
      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: hogehoge
          file: ./docker/Dockerfile
          push: true
          tags: "${{ env.IMAGE_NAME }}:${{ env.TAG }}"
          labels: ${{ steps.meta.outputs.labels }}
          build-args: |
            NEXT_PUBLIC_EXAMPLE_ENV=${{ env.NEXT_PUBLIC_EXAMPLE_ENV }}

DockerfileARG コマンドと ENV コマンドで環境変数を外から指定できるようにしておく必要があります。

ARG NEXT_PUBLIC_EXAMPLE_ENV=http://example.com
ENV NEXT_PUBLIC_EXAMPLE_ENV=${NEXT_PUBLIC_EXAMPLE_ENV}
...

release-please.yml

Release Please の yml ファイルです。Version タグを打った Docker イメージも生成するようにしています。

name: release-please

env:
  IMAGE_NAME: hogehoge

on:
  push:
    branches:
      - main

permissions:
  contents: write
  pull-requests: write

jobs:
  release-please:
    runs-on: ubuntu-latest
    steps:
      - name: Run release-please
        id: release
        uses: google-github-actions/release-please-action@v3
        with:
          release-type: go
          package-name: "hogehoge"
      - name: Set TAG from release-please
        if: ${{ steps.release.outputs.release_created }}
        run: |
          echo "Release Tag - ${{ steps.release.outputs.tag_name }}"
          if [ -n "${{ steps.release.outputs.tag_name }}" ]; then
            echo "TAG=${{ steps.release.outputs.tag_name }}" >> $GITHUB_ENV
          else
            echo "TAG=latest" >> $GITHUB_ENV
          fi
      - name: Checkout for tag created
        if: ${{ steps.release.outputs.release_created }}
        uses: actions/checkout@v4
      - name: Docker meta for tag created
        if: ${{ steps.release.outputs.release_created }}
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${IMAGE_NAME}
      - name: Login to DockerHub for tag created
        if: ${{ steps.release.outputs.release_created }}
        uses: docker/login-action@v3
        with:
          username: fugafuga
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Build and push for tag created
        if: ${{ steps.release.outputs.release_created }}
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: "${{ env.IMAGE_NAME }}:${{ env.TAG }}"
          labels: ${{ steps.meta.outputs.labels }}

lint-PR-title.yml

プルリクのタイトルが Conventional Commits に従っているかチェックするための yml ファイルです。 https://github.com/dreampulse/action-lint-pull-request-title をそのまま使っています。

github.com

name: "Lint PR Title"
on:
  pull_request_target:
    types:
      - opened
      - edited
      - synchronize

jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: dreampulse/action-lint-pull-request-title@master
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

auto-test.yml

テストを実行するための yml ファイルです。このあたりは各プロジェクトでいい感じに。

name: Lint, Test & Build

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  lint-test-build:
    runs-on: ubuntu-latest
    steps:
      - name: Setup Go
        uses: actions/setup-go@v4
        with:
          go-version: ^1.21
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Lint
        uses: golangci/golangci-lint-action@v3
        with:
          version: latest
          args: --timeout=5m
      - name: Test
        run: make test # make コマンドを使用してテストしている場合
      - name: Build 
        run: go build .