はじめに
Spring Boot + React の小さなアプリを勉強で作っています。
一度、手作業で EC2 にデプロイしました。SSHして、Docker入れて、EC2の中でビルドして…と、その道中でメモリ不足やらビルドツール不足やらの罠を踏みまくりました。そこで二周目は Terraform + user_data で「terraform apply 1発」の自動デプロイに作り直すことに。
面白かったのは、一周目で踏んだ罠の在り処を全部知っていたこと。おかげで二周目は、同じ穴の手前で全部よけられました。遠回りに見えた一周目が、そのまま二周目の地図になった記録です🗺️
※「apply 1発」と言っても、事前に イメージのビルド → ECRへpush ・
terraform init・ AWS認証の設定 は済ませてあります。ここでの「1発」は、ECRにイメージがある状態から、EC2の構築〜起動までを自動化した、という意味です。
環境
- Java 21 / Spring Boot 3.5系(Docker化済み)
- AWS(EC2 / ECR / IAM)
- Terraform 1.x
- ローカルは Apple Silicon(arm64)の Mac
設計の核:EC2では「ビルドしない」
一周目は EC2 の中で Gradle ビルドをして、1GBメモリのインスタンスでメモリ不足寸前になりました。二周目は発想を変えます。
- 先に手元でイメージをビルド → ECR(AWSのプライベートな倉庫)に push
- EC2 は起動時に ECR から pull して動かすだけ(=EC2ではビルドしない)
これで「重いビルドをEC2でやる」悩みが丸ごと消えます。
罠①:CPUアーキの違い(手元で焼くなら最重要)
手元の Mac は arm64、EC2 は x86_64。何も考えずにビルドすると arm64 のイメージができ、EC2 では exec format error で動きません。
→ ビルド時に --platform linux/amd64 を付けて x86_64 向けに焼く。
docker build --platform linux/amd64 \
-t <ECRのURL>:latest ./backend
=> [backend build 4/5] RUN ./gradlew bootJar
=> => # BUILD SUCCESSFUL in 59s
=> exporting to image
=> => naming to <ECRのURL>:latest ✔ DONE
x86_64 で焼けたイメージを ECR に push しておきます。
罠②:コンテナ内の「localhost」はDBに届かない
今回は EC2上の Docker Compose で、アプリコンテナと DBコンテナ(PostgreSQL)を同じネットワークで動かしています(DBはRDSではなく同居コンテナ)。この構成だと、アプリの接続先が localhost だとコンテナ自身を指してDBに届きません。
→ compose の環境変数で DBの宛先をサービス名(db)に上書き。
backend:
image: <ECRのURL>:latest
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/appdb # localhost ではなく db
depends_on: [db]
罠③:鍵を機械に置きたくない
EC2 が ECR から pull するには認証が要りますが、アクセスキーを user_data に直書きするのは危険。
→ IAMロールを EC2 に付ける。一時資格が自動で配られ、長期の鍵を機械に置かずに済みます(Terraform では aws_iam_role + aws_iam_instance_profile、ポリシーは ECR 読み取り専用)。
罠④:user_data の中の YAML が崩れる
user_data に compose(YAML)を直書きすると、Terraform のヒアドキュメントで字下げが崩れがち(YAMLは字下げが命)。
→ 別のテンプレートファイルに出して templatefile() で読み込む。
user_data = templatefile("${path.module}/user_data.sh.tftpl", {
image = "${aws_ecr_repository.backend.repository_url}:latest"
})
user_data は初回起動時に走ります。うまく立ち上がらない時は、EC2 内の
/var/log/cloud-init-output.logで出力とエラーを確認しました。
動かしてみる
terraform apply で yes。作られるのはEC2・SG・IAMロール一式:
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.
Outputs:
public_ip = "<公開IP>"
この時点ではEC2が起動しただけで、user_data はまだ裏で走っています(Docker導入→pull→compose)。1分ほど待って curl:
curl -s http://<公開IP>:8080/posts
[]
200 で [](新しいDBなので中身は空)。SSHを一度もしていないのに、サーバーが立ち上がってアプリまで動いている。これは気持ちいい。
⚠️ ここで開けている 8080 は学習用です。本番ではセキュリティグループで公開範囲を絞り、ALB や nginx での HTTPS 化を検討します。
使い終わったら片付け:
Destroy complete! Resources: 6 destroyed.
ただし AWS では destroy しても、ECRのイメージ保存料・EBSスナップショット・確保したままのElastic IP・ログなどが残ることがあります。料金を抑えるには「不要なものが残っていないか」も確認します(今回は ECR を force_delete で一緒に消す設定にしました)。
学び
- EC2でビルドしない(事前ビルド→ECRからpull)だけで、メモリ不足やビルドツールの悩みが消える
-
arch違いは
--platformで先回り(忘れるとexec format error) - 権限は鍵でなく IAMロールでマシンに与える
- 片付けは「destroyして終わり」ではなく、残りリソースも確認する
- 手で一度やってからコード化すると、罠の位置が分かっていて先回りできる
おわりに
エラーに突っ込んでから直す一周目も勉強になりますが、罠の地図を持って二周目を走るのは、また別の気持ちよさがありました。急がば回れ、を地で行った感じです。
同じく「自動デプロイ、何から…」で止まっている人の最初の一歩になれば嬉しいです🙌