🐈

Github ActionsでDBマイグレーションを自動化する part2

2024/12/19に公開

この記事は、tacoms Advent Calendar 2024の18日目です!
他メンバーのAdvent Calendarはこちらからご覧ください!👇
https://qiita.com/advent-calendar/2024/tacoms

こんにちは、株式会社tacoms SREの はぶちん(@modokkin) です。
前回は既存のマイグレーションプロセスの課題とGitHub Actionsでどのように承認フローを実現するかについて解説にしました。よければ前回の記事もあわせてご覧ください。

Part2では、手動トリガーでDry runを実行できるようにする方法について解説します。

開発の流れ

まず、sql-migrateをどこで実行するかですが、経験上 ecspresso + AWS Fargate で構築する事が手っ取り早いことを知っていたので、この構成を採用しました。
Fargateタスクの実行はもちろんAWS CLIだけで実現することもできるのですが、ecspressoを活用すればFargateタスクを起動するまでのあれこれを省略することが可能です。

構成が決まったので、次に手動トリガーでsql-migrateのstatus確認とDry runができる状態を目指します。
それが出来たらPull Request(以下PR)への組み込み、マイグレーションの適用ワークフローの構築を徐々に進めていきます。

GitHub Actionsにおけるワークフローのトリガー

GitHub Actionsには、さまざまな実行トリガーが用意されているのですが、事前知識として今回扱うトリガーについてピックアップします。ご存知の方は読み飛ばしてください。

  • pull_request_target
    特定のベースブランチ(マージ先)向けのPRを作成した際や、ヘッドブランチ(マージ元)へのプッシュでトリガー。マージ前のテストやセキュリティスキャンに利用。

  • pull_request
    PRの作成、更新、マージなどでトリガー。コードレビューの自動化やコードフォーマットチェックに利用。

  • workflow_dispatch
    手動でトリガー。任意のタイミングでワークフローを実行したい場合に利用。

他にもいろいろあるので、詳細は公式ドキュメントをご覧ください。
https://docs.github.com/ja/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows

sql-migrateのビルドからDry run実行まで

とりあえず手動でDry runが実行できることを目標に準備を進めます。
この記事では割愛しますが、ECRリポジトリやFargateの実行に必要なリソース(ロール、ネットワーク周り、Logs周りとか)は事前にterraformで作成しました。参考にされる場合は、事前に準備してください。

各種ファイルの内容はブログ向けに生成したものなので、一部そのまま使えない可能性がある点を予めご了承ください!

ディレクトリ構成
.
├── Docker
│   └── Dockerfile
├── ecspresso
│   ├── ecs-service-def.json
│   ├── ecs-task-def.json
│   ├── ecspresso.yml
│   └── overrides.json
└── scripts
    ├── dbconfig.yml
    ├── migrations
    │   └── 202412180000-sample.sql
    ├── mysql-dryrun.sh
    └── mysql-up.sh

Dockerfileとsql-migrate実行に必要なファイル

着手する時点で sql-migrate は、ビルドすらまともにできない状態だったので、まずはDockerfileを作成します。
実行コマンドは後ほどGitHub Actionsのワークフロー側で調整しやすいようにシェルスクリプトにしました。

Dockerfile
FROM golang:1.22.10

WORKDIR /go/src/github.com/example/sqlmigrate

RUN go install -v github.com/rubenv/sql-migrate/...@v1.7.0

# Copy Directory
COPY ./scripts/ ./
RUN chmod +x ./*.sh
RUN ls -la

# Execute command
// 後々上書きしたいのでCMDを利用します
CMD ["./mysql-dryrun.sh", "-config=dbconfig.yml", "-env=development"]

mysql-dryrun.sh
// シェバンは間違え無い
#!/bin/bash
// エラーがあったら止まってほしいのでsetを利用
set -ouex

// スクリプト実行時に引数があったらそれを使う
OPTIONS=${@:-"-config=./dbconfig.yml -env=development"}

// statusで状態を確認し、up -dryrunでDryrunを実行
sql-migrate status $OPTIONS && \
sql-migrate up -dryrun $OPTIONS

db-config.yml
development:
    dialect: mysql
    datasource: ${DB_USER}:${PASSWORD}@tcp(${HOST}:3306)/${DATABASE}?parseTime=true
    dir: ./migrations

202412180000-sample.sql
-- +migrate Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE people (id int);

-- +migrate Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE people;

ecspressoを実行するために必要なファイル

ecspresso自体の使い方については、とても読みやすい公式ドキュメントがあるのでそちらを参考にしてください。
(ecspressoのドキュメントは一部を除いて有料ですが、非常に有益なので今後ecspressoを活用する方は課金することをおすすめします!)
https://zenn.dev/fujiwara/books/ecspresso-handbook-v2/viewer/tutorial

.terraform_versionみたいなやつです。

2.4.2

ecspressoの設定ファイル

ecspresso.yml
region: ap-northeast-1
cluster: example-cluster
service_definition: ecs-service-def.json
task_definition: ecs-task-def.json
timeout: "10m0s"

設定ファイルから参照するサービス定義(ecspressoがネットワーク周りとかを参照する仕様なので必要)

ecs-service-def.json
{
  "deploymentConfiguration": {
    "deploymentCircuitBreaker": {
      "enable": true,
      "rollback": true
    },
    "maximumPercent": 200,
    "minimumHealthyPercent": 100
  },
  "deploymentController": {
    "type": "ECS"
  },
  "desiredCount": 1,
  "enableECSManagedTags": false,
  "enableExecuteCommand": true,
  "healthCheckGracePeriodSeconds": 0,
  "launchType": "FARGATE",
  "loadBalancers": [],
  "networkConfiguration": {
    "awsvpcConfiguration": {
      "assignPublicIp": "DISABLED",
      "securityGroups": [
        "{securityGroupID}"
      ],
      "subnets": [
        "{SubnetID}"
      ]
    }
  },
  "platformFamily": "Linux",
  "platformVersion": "LATEST",
  "propagateTags": "NONE",
  "schedulingStrategy": "REPLICA",
  "tags": []
}

設定ファイルから参照するタスク定義

ecs-task-def.json
{
  "containerDefinitions": [
    {
      "command": [],
      "cpu": 1024,
      "environment": [
        {
          "name": "DB_HOST",
          "value": "example-cluster.cluster-abcdefghijk.ap-northeast-1.rds.amazonaws.com"
        },
        {
          "name": "AWS_REGION",
          "value": "ap-northeast-1"
        },
        {
          "name": "DB_USER",
          "value": "user"
        },
        {
          "name": "DATABASE",
          "value": "example"
        }
      ],
      "essential": true,
      "image": "{{ must_env `IMAGE_URI` }}",
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/logs/example",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "ecs"
        }
      },
      "memory": 2048,
      "name": "sql-migrate",
      "portMappings": [],
      "secrets": [
        {
          "name": "DB_PW",
          "valueFrom": "arn:aws:ssm:ap-northeast-1:{AWS_ACCOUNT_ID}:parameter/example/DB_PW"
        }
      ],
      "ulimits": []
    }
  ],
  "cpu": "1024",
  "executionRoleArn": "arn:aws:iam::{AWS_ACCOUNT_ID}:role/example-task-excution-role",
  "family": "sql-migrate",
  "ipcMode": "",
  "memory": "2048",
  "networkMode": "awsvpc",
  "pidMode": "",
  "requiresCompatibilities": [
    "FARGATE"
  ],
  "tags": [],
  "taskRoleArn": "arn:aws:iam::{AWS_ACCOUNT_ID}:role/example-task-role"
}

設定を上書きするoverridesファイルを分けておくことで可変性を持たせています。

overrides.json
{
    "containerOverrides": [
        {
            "name": "{{ must_env `CONTAINER_NAME` }}",
            "command": [
                "{{ must_env `COMMAND` }}",
                "-config=dbconfig.yml",
                "-env={{ must_env `ENV` }}",
                "{{ env `ARG` ``}}"
            ]
        }
    ]
}

GitHub ActionsのDry runワークフロー

ここからはGitHub Actionsのワークフローの実装について触れていきます。
多分そのままでも動くと思いますが、環境変数周りはGitHubの機能を活用したほうが良いと思います。

execute-sql-migrate-dryrun.yml
name: execute-sql-migrate-dryrun
run-name: execute sql-migrate dryrun
on: 
  workflow_dispatch:
concurrency:
  group: ${{ github.workflow }}-${{ github.event.number }}
  cancel-in-progress: true
env:
  ENV: development
  ROLE_ARN: 'arn:aws:iam::{AWS_ACCOUNT_ID}:role/{ROLE_NAME}'
  CONTAINER_NAME: sql-migrate
  ECSPRESSO_CONFIG: ecspresso.yml

jobs:
  dryrun:
    name: Build and dryrun
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read

    steps:
      - name: Checkout base branch
        uses: actions/checkout@v4
      # OIDCでIAM Roleを利用
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ env.ROLE_ARN }}
          aws-region: ap-northeast-1
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2
      # マイグレーションファイルをイメージに含めているので、毎回ユニークになるようにする
      - name: Set up meta data # Dockerイメージにタグを付与
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: |
            ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}
          tags: |
            value=${{ github.sha }}
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # AWSのECRのレジストリ名を取得
          ECR_REPOSITORY: ${{ env.CONTAINER_NAME }}
      - name: Build and push images
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64
          file: Docker/Dockerfile
          tags: ${{ steps.meta.outputs.tags }}
          push: true
      # 公式のActionsありがたいですね
      - name: Install ecspresso
        uses: kayac/ecspresso@v2
        with:
          version-file: ./ecspresso/.ecspresso-version
      - name: Execute sql-migrate status and dryrun
        working-directory: ./ecspresso
        run: |
          ecspresso run --config=${{ env.ECSPRESSO_CONFIG }} --overrides-file=overrides.json
        env:
          IMAGE_URI: ${{ steps.meta.outputs.tags }}
          CONTAINER_NAME: ${{ env.CONTAINER_NAME }}
          COMMAND: "./mysql-dryrun.sh"
          ENV: ${{ env.ENV }}
          ARG: ""

前回の記事で紹介したCode Ownersの設定ファイルも作成しておきます。

CODEOWNERS
/.github/CODEOWNERS @example-org/db-admins
/.github/workflows/*sql-migrate* @example-org/db-admins
/scripts/ @example-org/db-admins

工夫したポイント

  • マイグレーションファイルは、S3などに置いておいて取ってくることもできると思いますが、今回はdockerイメージに埋め込んでいます。その代わりイメージを判別できるようにする必要があるので、イメージタグにgitのコミットハッシュを付与しています。
  • ecspressoのrunオプションを利用することでFargateでタスク実行時ハンドリングをActionsで気にする必要が無いのでとても楽ちんです。
  • ecspressoに渡す引数を可変にすることで、トラブルシューティング時にdebugオプションを付けたり、別環境に適用したりと柔軟性を持たせました。ただ、これはリポジトリ権限管理を適切に行なっていないとセキュリティリスクにつながるのでご注意ください。

手動実行手順

ここまでで作成したファイルをリポジトリのデフォルトブランチにマージすると、GitHub Actionsのワークフローリストに作成したワークフローが表れます。その後、ブランチを選んで実行するとActionsの実行画面でecspressoの実行ログが確認出来ます。

ecpresso runの実行結果
+ sql-migrate status -config=dbconfig.yml -env=development
+----------------------------+-------------------------------+
|          MIGRATION         |            APPLIED            |
+----------------------------+-------------------------------+
| 202412180000-sample.sql    | 2024-12-18 00:00:00 +0000 UTC |
+----------------------- ----+-------------------------------+
+ sql-migrate up -dryrun -config=dbconfig.yml -env=development
==> Would apply migration 202412180000-sample.sql (up)
CREATE TABLE people (id int);
/example-cluster Run task completed!

workflow_dispatchの詳細は公式ドキュメントをご覧ください。
https://docs.github.com/ja/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/manually-running-a-workflow

workflow_dispatchで気になった点

ワークフローを手動実行する際は、実行元ブランチを選択することになるのですが、公式の機能としてブランチを制限するような機能は今のところ無さそうでした。制限したい場合はワークフロー内でブランチを判定するような処理が必要になるようです。

つづく

今回の記事はここまでです。次の記事では以下のような内容について触れたいと思いますので、お楽しみに!(年明けになっちゃうかも)

  • マイグレーション適用時にユーザーを制限する方法。
  • PullRequestにDry runワークフロー結果をコメントする方法。
tacomsテックブログ

Discussion