概要
Terraformを用いて、AWS ECRへのコンテナイメージのビルドとAppRunnerのデプロイを自動化します。
今回は以下の環境で行います。
- Java 21
- Micronaut
- 標準でdockerビルドに対応してるのでこれを採用
AppRunnerについて
AppRunnerとは、Webアプリケーションを簡単にデプロイし、オートスケーリングやロードバランシングや暗号化など、Webアプリケーション運用に必要な諸々のことを自動で行ってくれるサーバーレスサービスです。
他のクラウドの類似サービスは以下になります。
- GCP: Croud Run
- Azule : Azure Container Apps
正直、この手のサービスだと先発の Cloud Run が一番高機能なんですが、AppRunnerも既に商用のWebアプリケーションを作るのに充分な機能を提供しています。
(ただ、まだWebアプリケーションに特化しており、Cloud Run のようにバッチ処理を行うような使い方は不向きです)
この手のサービスの最大の特徴は、Dockerイメージをpushすることで自動でデプロイができる点でしょうか。
デプロイもローリングアップデートで徐々にトラフィックを変えてくれるという形をとるので、push後は何も気にすることなく鼻をほじってたらデプロイが完了します。
また、AppRunnerはソースコードをpushすることでもデプロイする「コードビルド」にも対応しています。
とはいえDockerイメージの方が汎用性が高いので、今回はDockerイメージをpushしてデプロイする手順を踏みます。
というわけで、実装
実装内容は以下に公開しています。
サーバー側はMicronautのプロジェクトを作成した時にできる内容をちょろっと変更しただけのもので、メインはterraformの設定になります。
以下のようにするだけでAppRunnerがデプロイされます。
(要 terraform インストール、 aws cli プロファイルの設定)
$ cd ./terraform/apprunner/dev $ terraform apply
一度実行して環境が構築されたあとは、次からはソースコードに差分がある時だけpushされ、差分がないと何もしないということをterraform側が自動でやってくれます。
やってること
ECRとAppRunnerのデプロイ設定
DockerのイメージをpushするためのECRの設定を行っています。
resource "aws_ecr_repository" "apprun-ecr-repository" { name = "my-ecr-repository" image_tag_mutability = "MUTABLE" image_scanning_configuration { scan_on_push = true } }
name
は外だしした方がよさそうですけど、とりあえず決め打ちです。
次にAppRunnerの設定は以下で行っています。
# ロールの設定 resource "aws_iam_role" "apprunner_role" { name = "AppRunnerECRAccess" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Principal = { Service = "build.apprunner.amazonaws.com" } Action = "sts:AssumeRole" } ] }) } # ロールのポリシー設定 resource "aws_iam_role_policy_attachment" "apprunner_ecr_access_policy" { role = aws_iam_role.apprunner_role.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess" } # AppRunnerの設定 resource "aws_apprunner_service" "sample" { service_name = "sample" source_configuration { image_repository { image_repository_type = "ECR" image_configuration { port = "8080" } image_identifier = "${aws_ecr_repository.apprun-ecr-repository.repository_url}:latest" } auto_deployments_enabled = true authentication_configuration { access_role_arn = aws_iam_role.apprunner_role.arn } } depends_on = [null_resource.deploy] }
特にそんなに言うこともないですが、その後ECRへのpushを行うタスク ( null_resource.deploy
)のあとに動くようにするよう、 depends_on
の設定をしています。
DockerのビルドとECRへのpush
次に、以下でDockerのビルドを行っています。
今回は Micronaut
を利用しているため、標準で入っている dockerBuild
の gradleコマンドを用いてビルドしています。
data "archive_file" "deploy" { type = "zip" output_path = "${path.module}/../../../build/hello.zip" source_dir = "${path.module}/../../../src" # .dockerignore 相当の指定を行う。 excludes = setunion( fileset("${path.module}/../../..", "test/**/*"), ) } resource "null_resource" "deploy" { provisioner "local-exec" { command = <<BASH cd ../../../ echo "Create Docker Image" ./gradlew dockerBuild # 最新のトークンを取得 LOGIN_PASSWORD=$(aws ecr get-login-password --region $AWS_REGION) if [ $? -ne 0 ]; then echo "Failed to get ECR login password" exit 1 fi echo "Deploy Docker Image to ECR" # ECRにログイン echo $LOGIN_PASSWORD | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com docker tag ${var.imagename}:latest $REPOSITORY_URL:latest docker push $REPOSITORY_URL":latest" BASH environment = { AWS_ACCOUNT_ID = data.aws_caller_identity.current.account_id AWS_REGION = data.aws_region.current.name REPOSITORY_URL = aws_ecr_repository.apprun-ecr-repository.repository_url } } triggers = { sha256 = "${data.archive_file.deploy.output_sha}" } }
archive_file
でzipファイルを作ってますが、これは単にコードの差分をsha256で検知するために利用してます。こちらの記事:Terraformでコンテナイメージのビルドからデプロイまでを行う(AWS編)を参考にさせてもらいました。
(docker_registry_imageを使う方がスマートだなと思ったんですが、ビルドも gradleを使ってるし素直にコマンドでpushまでするようにしてます)
環境の設定
terraformは未だに環境ごとに設定を分けるベストプラクティスがよくわかってないのですが(おすすめとかあれば教えてほしいです)、ぼくは基本的に基本的な処理を全部 common
ディレクトリに入れ、環境ごとのディレクトリでそのモジュールを呼び出すという形で実現してます。
(AWSの場合は環境ごとにディレクトリを分けたりはせずに、実行の際都度プロファイルを入力する感じで運用する方がいいのかもしれないですが)
というわけで、 terraform/apprunner/dev
というディレクトリを作り、そこで環境設定を行っています。
locals { profile = "admin" } provider "aws" { region = "ap-northeast-1" profile = local.profile } # 処理の内容を呼び出す module "common" { source = "../common" }