7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

QualiArtsAdvent Calendar 2020

Day 6

GKEでのDBマイグレーション自動化手法

Last updated at Posted at 2020-12-05

QualiArts Advent Calendar 2020、6日目担当の8kkaです。
今回はGKE環境でのDBマイグレーション自動化手法について書こうと思います。

1. 前提の設計など

今回は以下の構成で組まれているシステムに対して、自動化手法を組み込んでいきます。

  • アプリケーションはGoで記述されている
  • GKE上で稼働させている
  • CDはArgoCDを利用している
  • DBはCloudSQLを利用している

紹介しているコードやアーキテクチャは、実際に構築してみたものを記事用に編集して掲載しています。

ライブラリやツールの紹介をしつつ記述していますが、アーキテクチャの全体像だけ知りたい方は4. アーキテクチャと処理フローの画像だけ参照すると何となくイメージは掴めると思います。

2. マイグレーション実行用のコンテナイメージ作成

マイグレーションの処理には、golang-migrateというライブラリを利用します。
まずはこのライブラリを使ってマイグレーションを実行するコンテナイメージを作成します。

2.1 golang-migrate

golang-migrateは、Go言語で記述されたDBマイグレーションのライブラリです。
Goのライブラリとしても使えるし、CLIから実行する事も出来ます。
マイグレーションファイルはローカルからだけではなく、GitHubやS3、GCSから取得する事も出来ます。
また、利用可能なDBが多い事も特徴で、2020/12/06現在では以下のDBが対応されています。

  • PostgreSQL
  • Redshift
  • Ql
  • Cassandra
  • SQLite
  • SQLCipher
  • MySQL/ MariaDB
  • Neo4j
  • MongoDB
  • CrateDB
  • Shell
  • Google Cloud Spanner
  • CockroachDB
  • ClickHouse
  • Firebird
  • MS SQL Server

2.2 マイグレーションファイルの構成

公式が推奨するマイグレーションファイルの構成(ファイル名)は以下になります。

{version}_{title}.up.{extension}
{version}_{title}.down.{extension}

upはバージョンを1つ上げる際に使用されるファイル、downは1つ下げる際に使用されるファイルです。
空のファイルを作成した場合は、空のクエリを実行しようとするので注意が必要です。
{version} の命名については 1_master.up.sql, 2_master.up.sql のような連番にする方法や、タイムスタンプを用いる方法があります。
複数のリリースバージョンの開発が並行して進む場合があるため、リリースバージョンに合わせて {major}{minor}{patch}_schema.up.sql という規則で作成してみます。

v1.0.0 -> 001000000_schema.up.sql
v1.1.1 -> 001001001_schema.up.sql

例えば、v1.0.0でitemテーブルを作成してv1.1.1でvalueカラムを追加する際は、以下のようなファイル名とSQLを記述します。

001000000_schema.up.sql
CREATE TABLE IF NOT EXISTS `item` (
  `id` varchar(255) NOT NULL COMMENT 'アイテムID',
  `name` varchar(255) NOT NULL COMMENT '名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
COMMENT='アイテム';
001001001_schema.up.sql
ALTER TABLE item ADD COLUMN value INT NOT NULL COMMENT '効果値' AFTER name;

v1.0.0のアプリがデプロイされている環境では001000000_schema.up.sqlで定義されているitemテーブルがマイグレートされ、v1.1.1のアプリがデプロイされている環境では、valueカラムが追加されたitemテーブルがマイグレートされます。

golang-migrateでDBのマイグレーションを実行すると、対象のDBにschema_migrationsというテーブルが作成され、そのテーブル内で現在のバージョンを管理することになります。
schema_migrationsはMySQLに対して実行した場合の名前で、CloudSpannerに実行した場合はSchemaMigrationsになるなど、DBによって命名は多少変わります。

2.3 実行用のコンテナイメージ

DBマイグレーションの実行は、Goで作ったアプリケーションをコンテナ化してArgoCD上のJobとして実行します。
コンテナ化するアプリケーションは以下のようなコードです。

package main

import (
	"context"
	"log"

	"github.com/golang-migrate/migrate/v4"
	_ "github.com/golang-migrate/migrate/v4/database/mysql"
	_ "github.com/golang-migrate/migrate/v4/source/file"
	"golang.org/x/sync/errgroup"
)

func main() {
	eg, _ := errgroup.WithContext(context.Background())
	eg.Go(func() error {
		if err := migrateMySQL(); err != nil {
			return err
		}
		return nil
	})
	if err := eg.Wait(); err != nil {
		log.Panic(err)
	}
}

func migrateMySQL() error {
	m, err := migrate.New(
		"file://./db/ddl/master",
		"mysql://{user}:{password}@tcp({port})/{db}",
	)
	if err != nil {
		return err
	}
	if err := m.Up(); err != nil && err != migrate.ErrNoChange {
		return err
	}
	return nil
}

Goroutineを使用しているのは、複数のデータベースに対してマイグレーションを実行する際に並列に処理したいためです。
(今回省略していますが、複数のDBを利用する要件もありました。)

migrate.ErrNoChangeのエラー判定を除外しているのは、「マイグレーション実行時にスキーマ変更がなかった場合エラーとして終了させないようにする」ための記述です。
例えばv1.0.0からv1.1.0に上げる場合、カラムが追加されるのはv1.1.1からなのでスキーマに変更が入らず、エラーとなってしまいます。
こちらを回避するため、migrate.ErrNoChangeの判定を実装しています。

上記アプリケーションをリリースバージョンのタグをつけてコンテナイメージ化します。
コンテナイメージはCloudBuildで作成し、ContainerRegistryに保存しておきます。
(ArtifactRegistryも使ってみたいですが、現状まだ触れていません。)

作成したマイグレーションファイルは./db/ddl/masterに保存しておき、コンテナ内に一緒に詰めておきます。
イメージにタグを付ける際、タグバージョンとコンテナ内に保存されているマイグレーションファイルの最新バージョンが同じものとなります。

例: v1.1.0のコンテナ内マイグレーションファイル
./db/ddl/master/001000000_schema.up.sql

例: v1.1.1のコンテナ内マイグレーションファイル
./db/ddl/master/001000000_schema.up.sql
./db/ddl/master/001001001_schema.up.sql

3. ArgoCDを使ったマイグレーションの自動実行

作成したコンテナイメージはGKE上のArgoCDを通して実行します。
ここでは、ArgoCDの ResourceHooks という仕組みを使って、GKEへのアプリケーションデプロイ前にマイグレーションを実行する方法を記述します。

3.1 ArgoCD Resource Hooks

ArgoCDにはResourceHooksという仕組みがあり、同期の操作前、操作中、操作後にスクリプトを実行する事ができます。
マニフェストの例はこちら。

apiVersion: batch/v1
kind: Job
metadata:
  generateName: db-schema-migrate
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: HookSucceeded

argocd.argoproj.io/hookでHookのタイミングを指定し、argocd.argoproj.io/hook-delete-policyでHookリソースの削除を設定できます。
このマニフェストの場合は、「同期前にJobが実行され、Jobが成功したらHookリソースを削除する」という挙動になります。

3.2 Hook Policy

argocd.argoprj.io/hookで設定できる項目は以下になります。

hook ポリシー 動作
PreSync マニフェスト適用前に実行
Sync PreSync完了時に実行
Skip マニフェスト適用をスキップ
PostSync マニフェスト適用に成功したら実行
SyncFail マニフェスト適用が失敗したら実行

今回はDBマイグレーションをマニフェスト適用前に実行したいので、PreSyncのポリシーを利用します。
また、argocd.argoprj.io/hook-delete-policyで設定できる項目は以下になります。

hook-delete ポリシー 動作
HookSucceeded フックで実行した処理が成功したらフックリソース削除
HookFailed フックで実行した処理が失敗したらフックリソース削除
BeforeHookCreation 新しいフックリソースが作られる前に既存のフックリソース削除

DBマイグレーションで実行したJobが成功した場合はリソースを削除し、失敗した場合は調査のためPodを残したいので、HookSucceededを利用します。

3.3 反映させるマニフェスト

実際に反映させるマニフェストは以下のような記述になります。

apiVersion: batch/v1
kind: Job
metadata:
  generateName: db-migrate
  namespace: job
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
  template:
    spec:
      containers:
      - name: db-migrate
        image: "gcr.io/test-app/github.com/qualiarts/migrate:v1.0.0"
        imagePullPolicy: Always
        command:
        - "/migrate"
      restartPolicy: Never
  backoffLimit: 0

Jobのリソースとして作成し、ResourceHooksのアノテーションを追加します。
実行はContainerRegistryに保存してあるマイグレーション用のイメージを利用します。
このマニフェストをArgoCDのApplicationリソースに紐付ける事で、ResourceHooksの仕組みが動作します。

4. アーキテクチャと処理フロー

全体のアーキテクチャと処理フローはこちらになります。
スクリーンショット 2020-12-06 3.16.47.png

app-serverにマイグレーションファイルを作成し、タグが切られたタイミングでCloudBuildが起動し、タグバージョンが指定されたコンテナイメージを作成します。
コンテナイメージ作成後、helmに記載されているアプリのバージョンを上げてmasterブランチにプッシュすると、ArgoCDが変更を検知してResourceHooksの処理が走ります。
ResourceHooksのマイグレーション処理が正常に完了すると、DBスキーマが更新されてからhelmの差分同期が走ります。

まとめ

golang-migrateとArgoCDのResourceHooksを利用する事で、DBのスキーママイグレーションを自動化する事が出来ました。
今回CloudSQLを例に挙げて記述しましたが、golang-migrateで対応されているCloudSpannerなど他のDBでも同じように構築する事が出来るので、似たような環境で構築されている方はぜひお試しください。
(一応、筆者が実際に確認したのはCloudSQLとCloudSpannerだけです。)

ここまで閲覧頂き、ありがとうございました。
明日は hikaru-suzuki さんの記事です。

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?