Instantly share code, notes, and snippets.

@nota-ja /1204.md Secret
Last active Dec 10, 2017

Embed
What would you like to do?
# Flyway の DB migration を CF のタスクとして *v3-cli-plugin なしで* 動かす

Flyway の DB migration を CF のタスクとして v3-cli-plugin なしで 動かす

この Gist は Cloud Foundry Advent Calendar 2017 の4日目の記事です。


1 なぜこの話を書くのか

本題の前置きが長くなるが,これを書いておかないと「何でこんなことをしているのか」が伝わらないと思うので,理由を書いておく。「そんなこと知ってるよ」という方は本題まで読み飛ばしてほしい。

1.1 CF におけるタスクとは

Cloud Foundry (以下CF) では,約1年くらい前 [^1] から task (以下「タスク」) という機能が使えるようになった。

[^1]: 少し調べてみたが,cli v6.23.0 のリリースノート によるとどうやら cf-release v247 から正式導入されたらしい。

CFにアプリをデプロイすると,コンテナーが作られ,その中でアプリが起動する。通常,それらのアプリ及びコンテナーは,ユーザーが明示的にアプリを停止するまで存在し続ける。このようなアプリは LRP (Long Running Process) と呼ばれている。

これに対しタスクは,起動されるとコンテナーが作られ,そこでプロセスが起動されるところまでは同じだが,所定の処理が終わったらプロセスは終了し,コンテナーも破棄される。

The Twelve-Factor App の12番目の項目として, 管理プロセス (Admin Processes) というものがあるが,CF のタスクはこれを実現するものである。

従って,その用途は上述の管理プロセスのページに書かれているような,

  • データベースのマイグレーション
  • REPL シェル
  • その他1回限りのスクリプトの実行

などである。

さらに,同ページに書かれている「1回限りの管理プロセスは、アプリケーションの通常の長時間実行されるプロセスと全く同じ環境で実行されるべきである」という条件を満たすべく,CF のタスクは,1つの LRP アプリの付属物として実行される造りになっている。

1.2 なぜ Flyway の DB migration について書くのか

こうした CF のタスクの性質を踏まえた上で,なぜ Flyway の DB migration の実行方法について書くことにしたのか。そんなことは,Java の Web アプリ (LRP) を CF 上にデプロイして, mvn flyway:migrate なり gradle flywayMigrate なりをタスクとして実行すれば済む話ではないのか。

しかし,実は CF の Java buildpack のある性質により,Web アプリ (LRP) の付随タスクとして DB migration を実行するのは難しい。

CF の Java buildpack は,コンパイル&パッケージ済みの JAR / WAR / ZIP ファイルのみを Java アプリとして受け付ける。この結果,ステージング後のアプリ実行環境では (通常) maven も gradle も実行できない。

他の言語の buildpack であれば,ソースコードのリポジトリーを「アプリ」として受け取って,そこからステージングを行うので,そこから bin/rails db:migratepython manage.py upgrade の実行も可能だが,Java buildpack ではそれが難しいのである。

もちろん,maven や gradle の実行環境を JAR / WAR パッケージに入れてしまうのも一つの方法だが,かさばる上に PATH の問題もあってそう単純にはいかない。

ではどうするか。その解を示してくれているのが, making さんの cf-task-flyway-migration である。

これは,DB migration を Web アプリの付随タスクとして動かすのではなく,タスク用の独立した LRP アプリを CF 上にデプロイし,そこで migration を実行する仕組みになっている (Twelve-factor 的にはNGだが,実は他言語 buildpack でも同様のアプローチを取っている例もあるので,DB migration は比較的環境依存性が小さいからOK,ということなのかもしれない)。

ただ残念なことに,このリポジトリーは1年以上前に作られて,更新も行われていないため,当時 (この目的を達成するために) まだ必要だった v3-cli-plugin に依存した方法で書かれている。

そこで「v3-cli-plugin に依存しない方法を改めて記述したかった」というのが,この文章を書いた理由である。長かった。

2 Flyway migration タスクの実行

ここからようやく本題に入る。

2.1 試した環境

  • CF API Version: 2.75.0 / 3.10.0
  • cf CLI 6.26.0+9c9a261fd

諸般の事情により少し古いがご容赦を。

2.2 コードの修正

1箇所だけ,実コードの修正が必要だった。

diff --git Procfile Procfile
index e4c2728..276e144 100644
--- Procfile
+++ Procfile
@@ -1 +1 @@
-worker:
+web: nc -l -k $PORT

独立した LRP アプリの process type として, オリジナル では worker を指定しているが,今回試した環境ではうまくいかなかったので,CF の stemcell に入っている nc (netcat) を使ってダミーの web プロセスを起動することにした。

ただし, $PORT にアクセスが来てもこのプロセスは何の応答も返さないので,後で述べるように --no-route を指定してアクセスが来ないようにしている。

2.3 操作手順

2.3.1 TL;DR (いまさら)

./mvnw clean package -DskipTests=true
cf push flyway-migration -p target/flyway-migration-0.0.1-SNAPSHOT.jar --no-route --no-start
cf create-service mysql free demo-db
cf bind-service flyway-migration demo-db
cf start flyway-migration
cf run-task flyway-migration ".java-buildpack/open_jdk_jre/bin/java org.springframework.boot.loader.JarLauncher" --name migrate
cf logs flyway-migration --recent
cf run-task flyway-migration ".java-buildpack/open_jdk_jre/bin/java org.springframework.boot.loader.JarLauncher" --name migrate
cf logs flyway-migration --recent

以下詳細。

2.3.2 アプリの build と package

./mvnw clean package -DskipTests=true

ローカルから接続できる MySQL のデータベースを指定しないとテストが落ちるので,MySQL を建てるのが面倒な場合は -DskipTests=true する。ちなみにテストの中身は空。

2.3.3 アプリのデプロイ (起動はまだ)

cf push flyway-migration -p target/flyway-migration-0.0.1-SNAPSHOT.jar --no-route --no-start

アプリ名は適当に。

サービスをバインドしてからアプリを起動するので,この時点では起動しない (--no-start) 。

先に述べた通り,このアプリの web プロセスは,本物の web サーバーではなく応答も返さないため,アクセスしてもタイムアウトでエラーになるだけである。従って, --no-route を付けて,外からはアクセスが来ないようにする。

2.3.4 MySQL サービスの作成

既にサービスが存在する場合は, この項の作業は省略可能。

cf create-service mysql free demo-db

create-service する際, mysql のところには,適宜自分の環境で cf marketplace して見える MySQL サービスの名前を入れる。

2.3.5 MySQL サービスのバインドとアプリの起動

cf bind-service flyway-migration demo-db
cf start flyway-migration

2.3.6 タスクの実行と実行結果の確認 (1回目)

いよいよ本題のタスクを実行する。

cf run-task flyway-migration ".java-buildpack/open_jdk_jre/bin/java org.springframework.boot.loader.JarLauncher" --name migrate

run-task サブコマンドの

  • 1番目の引数には,アプリ名を
  • 2番目の引数には,タスクとして実行するコマンドを

指定する。

--name ... は省略可能。

上述のコマンドを実行すると,以下のような出力が返ってくるはずである:

buildpack/open_jdk_jre/bin/java org.springframework.boot.loader.JarLauncher" --name migrate
Creating task for app flyway-migration in org *** / space *** as ***...
OK

Task has been submitted successfully for execution.
task name:   migrate
task id:     1

タスクの実行は非同期に行われるので,この時点ではタスクが登録されたに過ぎず,まだ終わったわけではない。

タスクが終わるまでしばらく (数十秒〜1分程度) 待ってから,

cf logs flyway-migration --recent

を実行すると,タスクの実行ログを見ることができる。

成功していれば,以下のような出力が得られるはずである:

2017-11-22T23:44:41.46+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:41.462  INFO 6 --- [           main] nfigurationApplicationContextInitializer : Adding cloud service auto-reconfiguration to ApplicationContext
2017-11-22T23:44:41.49+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:41.490  INFO 6 --- [           main] com.example.FlywayMigrationApplication   : The following profiles are active: cloud
2017-11-22T23:44:41.49+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:41.490  INFO 6 --- [           main] com.example.FlywayMigrationApplication   : Starting FlywayMigrationApplication on 9f01f6af-a074-409e-a67b-cc14181a1c86 with PID 6 (/home/vcap/app/BOOT-INF/classes started by vcap in /home/vcap/app)
2017-11-22T23:44:41.57+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:41.575  INFO 6 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@46fbb2c1: startup date [Wed Nov 22 14:44:41 UTC 2017]; root of context hierarchy
2017-11-22T23:44:42.66+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:42.667  INFO 6 --- [           main] urceCloudServiceBeanFactoryPostProcessor : Auto-reconfiguring beans of type javax.sql.DataSource
2017-11-22T23:44:42.69+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:42.690  INFO 6 --- [           main] o.c.r.o.s.c.s.r.PooledDataSourceCreator  : Found Tomcat JDBC connection pool on the classpath. Using it for DataSource connection pooling.
2017-11-22T23:44:43.29+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:43.291  INFO 6 --- [           main] o.f.core.internal.util.VersionPrinter    : Flyway 3.2.1 by Boxfuse
2017-11-22T23:44:43.30+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:43.302  WARN 6 --- [           main] o.a.tomcat.jdbc.pool.ConnectionPool      : maxIdle is larger than maxActive, setting maxIdle to: 4
2017-11-22T23:44:43.91+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:43.919  INFO 6 --- [           main] o.f.c.i.dbsupport.DbSupportFactory       : Database: jdbc:mysql://10.0.16.48:3306/cf_d98d8e61_9559_452c_a950_8e50b1a3adcb?user=oXbdI6zCXpRXvorC&password=HSNAjfEO4bgBlyvc (MySQL 5.5)
2017-11-22T23:44:43.99+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:43.991  INFO 6 --- [           main] o.f.core.internal.command.DbValidate     : Validated 2 migrations (execution time 00:00.031s)
2017-11-22T23:44:44.03+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:44.034  INFO 6 --- [           main] o.f.c.i.metadatatable.MetaDataTableImpl  : Creating Metadata table: `cf_d98d8e61_9559_452c_a950_8e50b1a3adcb`.`schema_version`
2017-11-22T23:44:44.12+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:44.126  INFO 6 --- [           main] o.f.core.internal.command.DbMigrate      : Migrating schema `cf_d98d8e61_9559_452c_a950_8e50b1a3adcb` to version 1 - create-schema
2017-11-22T23:44:44.12+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:44.126  INFO 6 --- [           main] o.f.core.internal.command.DbMigrate      : Current version of schema `cf_d98d8e61_9559_452c_a950_8e50b1a3adcb`: << Empty Schema >>
2017-11-22T23:44:44.17+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:44.170  INFO 6 --- [           main] o.f.core.internal.command.DbMigrate      : Migrating schema `cf_d98d8e61_9559_452c_a950_8e50b1a3adcb` to version 2 - import-initial-data
2017-11-22T23:44:44.20+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:44.200  INFO 6 --- [           main] o.f.core.internal.command.DbMigrate      : Successfully applied 2 migrations to schema `cf_d98d8e61_9559_452c_a950_8e50b1a3adcb` (execution time 00:00.169s).
2017-11-22T23:44:44.34+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:44.345  INFO 6 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-11-22T23:44:44.38+0900 [APP/TASK/migrate/0] OUT {id=2, message=hello2}
2017-11-22T23:44:44.38+0900 [APP/TASK/migrate/0] OUT {id=3, message=hello3}
2017-11-22T23:44:44.38+0900 [APP/TASK/migrate/0] OUT {id=1, message=hello1}
2017-11-22T23:44:44.39+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:44.391  INFO 6 --- [       Thread-2] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@46fbb2c1: startup date [Wed Nov 22 14:44:41 UTC 2017]; root of context hierarchy
2017-11-22T23:44:44.39+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:44.393  INFO 6 --- [       Thread-2] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
2017-11-22T23:44:44.39+0900 [APP/TASK/migrate/0] OUT 2017-11-22 14:44:44.389  INFO 6 --- [           main] com.example.FlywayMigrationApplication   : Started FlywayMigrationApplication in 4.12 seconds (JVM running for 4.856)
2017-11-22T23:44:44.41+0900 [APP/TASK/migrate/0] OUT Exit status 0
2017-11-22T23:44:44.44+0900 [APP/TASK/migrate/0] OUT Destroying container
2017-11-22T23:44:45.18+0900 [APP/TASK/migrate/0] OUT Successfully destroyed container

2.3.7 タスクの実行と実行結果の確認 (2回目)

もう一度タスクを実行してみる。同じデータベースに同じ migration を適用した場合,2回目は migration が行われないはず。

cf run-task flyway-migration ".java-buildpack/open_jdk_jre/bin/java org.springframework.boot.loader.JarLauncher" --name migrate
Creating task for app flyway-migration in org *** / space *** as ***...
OK

Task has been submitted successfully for execution.
task name:   migrate
task id:     2

適当な時間待ってから,

cf logs flyway-migration --recent
(略)
2017-11-23T01:56:47.54+0900 [APP/TASK/migrate/0] OUT 2017-11-22 16:56:47.545  INFO 7 --- [           main] o.f.core.internal.command.DbMigrate      : Schema `cf_d98d8e61_9559_452c_a950_8e50b1a3adcb` is up to date. No migration necessary.
(略)

ログは先ほどと重なる部分が多く長いので省略したが,正しく動作していれば,上に示したような出力 (... is up to date. No migration necessary.) が得られるはずである。

3 終わりに

今回使ったコードを

に上げた。

4 注記

このアプリはあくまでデモ向けであり,実際の DB migration で使うためには,

の migration ファイルを置き換える必要があることはいうまでもない。

なお,これらに加えて,

も修正する必要がある。実はこの部分のコードは,上述の migration ファイルに依存したものになっているためである。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment