Miistin's Tech Blog

株式会社ミースチンの技術ブログ

AWS AppRunner のデプロイをTerraformで行う

概要

Terraformを用いて、AWS ECRへのコンテナイメージのビルドとAppRunnerのデプロイを自動化します。
今回は以下の環境で行います。

  • Java 21
  • Micronaut
    • 標準でdockerビルドに対応してるのでこれを採用

AppRunnerについて

aws.amazon.com

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してデプロイする手順を踏みます。

というわけで、実装

実装内容は以下に公開しています。

github.com

サーバー側は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"
}