この Gist は Cloud Foundry Advent Calendar 2017 の4日目の記事です。
本題の前置きが長くなるが,これを書いておかないと「何でこんなことをしているのか」が伝わらないと思うので,理由を書いておく。「そんなこと知ってるよ」という方は本題まで読み飛ばしてほしい。
Cloud Foundry (以下CF) では,約1年くらい前 1 から task (以下「タスク」) という機能が使えるようになった。
CFにアプリをデプロイすると,コンテナーが作られ,その中でアプリが起動する。通常,それらのアプリ及びコンテナーは,ユーザーが明示的にアプリを停止するまで存在し続ける。このようなアプリは LRP (Long Running Process) と呼ばれている。
これに対しタスクは,起動されるとコンテナーが作られ,そこでプロセスが起動されるところまでは同じだが,所定の処理が終わったらプロセスは終了し,コンテナーも破棄される。
The Twelve-Factor App の12番目の項目として, 管理プロセス (Admin Processes) というものがあるが,CF のタスクはこれを実現するものである。
従って,その用途は上述の管理プロセスのページに書かれているような,
- データベースのマイグレーション
- REPL シェル
- その他1回限りのスクリプトの実行
などである。
さらに,同ページに書かれている「1回限りの管理プロセスは、アプリケーションの通常の長時間実行されるプロセスと全く同じ環境で実行されるべきである」という条件を満たすべく,CF のタスクは,1つの LRP アプリの付属物として実行される造りになっている。
こうした 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:migrate
や python 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 に依存しない方法を改めて記述したかった」というのが,この文章を書いた理由である。長かった。
ここからようやく本題に入る。
- CF API Version: 2.75.0 / 3.10.0
- cf CLI 6.26.0+9c9a261fd
諸般の事情により少し古いがご容赦を。
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
を指定してアクセスが来ないようにしている。
./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
以下詳細。
./mvnw clean package -DskipTests=true
ローカルから接続できる MySQL のデータベースを指定しないとテストが落ちるので,MySQL を建てるのが面倒な場合は -DskipTests=true
する。ちなみにテストの中身は空。
cf push flyway-migration -p target/flyway-migration-0.0.1-SNAPSHOT.jar --no-route --no-start
アプリ名は適当に。
サービスをバインドしてからアプリを起動するので,この時点では起動しない (--no-start
) 。
先に述べた通り,このアプリの web
プロセスは,本物の web サーバーではなく応答も返さないため,アクセスしてもタイムアウトでエラーになるだけである。従って, --no-route
を付けて,外からはアクセスが来ないようにする。
既にサービスが存在する場合は, この項の作業は省略可能。
cf create-service mysql free demo-db
create-service
する際, mysql
のところには,適宜自分の環境で cf marketplace
して見える MySQL サービスの名前を入れる。
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
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
もう一度タスクを実行してみる。同じデータベースに同じ 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.
) が得られるはずである。
今回使ったコードを
に上げた。
このアプリはあくまでデモ向けであり,実際の DB migration で使うためには,
- src/main/resources/db/migration/V1__create-schema.sql
- src/main/resources/db/migration/V2__import-initial-data.sql
の migration ファイルを置き換える必要があることはいうまでもない。
なお,これらに加えて,
も修正する必要がある。実はこの部分のコードは,上述の migration ファイルに依存したものになっているためである。
Footnotes
-
少し調べてみたが,cli v6.23.0 のリリースノート によるとどうやら cf-release v247 から正式導入されたらしい。 ↩