aws

EC2からTerraformを使ってApp Runnerに移行しようと思ったが心折れた話

2024/08/01

はじめに

これはネタ記事です。
真面目に移行を検討されている方は他の記事を参照することをお勧めします。

一応、今回作成した terraform ファイルはこちらです。

provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_ecr_repository" "hoge_api_repo" {
  name = "hoge-api-repo"
}

resource "aws_iam_role" "apprunner_role" {
  name = "apprunner-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = ["apprunner.amazonaws.com", "build.apprunner.amazonaws.com"]
        },
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "apprunner_service_ecr" {
  role       = aws_iam_role.apprunner_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess"
}

resource "aws_iam_role" "apprunner_instance_role" {
  name = "apprunner-instance-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = "tasks.apprunner.amazonaws.com"
        },
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy" "apprunner_policy" {
  name = "apprunner-policy"
  role = aws_iam_role.apprunner_role.id

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = [
          "ecr:GetDownloadUrlForLayer",
          "ecr:BatchGetImage",
          "ecr:BatchCheckLayerAvailability",
          "apprunner:CreateService",
          "apprunner:DeleteService",
          "apprunner:DescribeService",
          "apprunner:ListServices",
          "apprunner:UpdateService",
          "iam:CreateServiceLinkedRole",
          "ecr:GetAuthorizationToken",
        ],
        Resource = "*"
      }
    ]
  })
}

resource "aws_apprunner_auto_scaling_configuration_version" "hoge_api_apprunner_autoscaling" {
  auto_scaling_configuration_name = "hoge-api-autoscaling"

  max_concurrency = 30
  max_size        = 1
  min_size        = 1
}

resource "aws_apprunner_service" "hoge_api_apprunner" {
  service_name = "hoge-api-apprunner"
  auto_scaling_configuration_arn = aws_apprunner_auto_scaling_configuration_version.hoge_api_apprunner_autoscaling.arn
  depends_on   = [time_sleep.wait_10_seconds]

  source_configuration {
    authentication_configuration {
      access_role_arn = aws_iam_role.apprunner_role.arn
    }

    image_repository {
      image_identifier      = "${aws_ecr_repository.hoge_api_repo.repository_url}:latest"
      image_repository_type = "ECR"
      image_configuration {
        port = "80"
        runtime_environment_variables = {
          ENV               = "production"
          POSTGRES_PORT     = "5432"
          POSTGRES_DB_NAME  = "hoge"
        }
      }
    }
  }

  network_configuration {
    egress_configuration {
      egress_type = "DEFAULT"
    }
  }

  instance_configuration {
    instance_role_arn = aws_iam_role.apprunner_instance_role.arn
    cpu = "1024"
    memory = "2048"
  }
}

背景

最近、個人開発で技術的チャレンジができていないなと思い、色々遊んでいました。
その 1 つが EC2 から App Runner 移行です。

アクセス数も多くないのに EC2 を使っているのがコスト的に気になったことと、
職場で Cloud Run を使っているので比較することで知見が増えるかなと思ったのが App Runner に移行を考えた理由です。

App Runner とは

厳密には違うかもしれませんが、GCP の Cloud Run の AWS 版という認識です。
Docker イメージなどコンテナを使ってウェブアプリケーションを簡単にデプロイできるフルマネージドサービスです。
App Runner を作成するためにはコンテナイメージを渡す必要があり、今回は DockerHub のようなコンテナイメージを配置する AWS サービスである ECR を使います。

下準備

terraform コマンドと aws コマンドを使えるようにしておきます。

terraform コマンド

brew コマンドでインストールします。

$ brew install hashicorp/tap/terraform
$ terraform -version
Terraform v1.9.2
on darwin_arm64

aws コマンド

まずは awscli 用のユーザーを作成します。
ブラウザから AWS にログインし、IAM の Users からユーザーを作成します。
そして、作成したユーザーの Security credentials からアクセスキーを作成します。

その後、brew コマンドでインストールします。

$ brew install awscli
$ aws --version
aws-cli/2.17.11 Python/3.11.9 Darwin/23.5.0 source/arm64

以下のコマンドを実行して、先ほど作成したアクセスキーなどを登録します。

$ aws configure

aws のポリシー

これから Terraform で様々なリソースを作っていく上で、上記で作成した IAM ユーザーに権限を付与します。
ブラウザから AWS にアクセスし、IAM -> Policies -> Create policy からポリシーを作成して、以下の JSON を貼り付けます。
※もしかしたら不要な権限も追加されているかもしれないです

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Action": [
				"ecr:CreateRepository",
				"ecr:DeleteRepository",
				"ecr:DescribeRepositories",
				"ecr:GetRepositoryPolicy",
				"ecr:SetRepositoryPolicy",
				"ecr:TagResource",
				"ecr:UntagResource",
				"ecr:ListTagsForResource",
				"ecr:GetAuthorizationToken",
				"ecr:InitiateLayerUpload",
				"ecr:UploadLayerPart",
				"ecr:BatchCheckLayerAvailability",
				"ecr:CompleteLayerUpload",
				"ecr:GetDownloadUrlForLayer",
				"ecr:BatchGetImage",
				"ecr:CompleteLayerUpload",
				"ecr:PutImage",
				"apprunner:CreateService",
				"apprunner:TagResource",
				"iam:CreateServiceLinkedRole",
				"iam:CreateRole",
				"iam:GetRole",
				"iam:ListRolePolicies",
				"iam:ListAttachedRolePolicies",
				"iam:ListInstanceProfilesForRole",
				"iam:DeleteRole",
				"iam:PutRolePolicy",
				"iam:PassRole",
				"iam:GetRolePolicy",
				"iam:DeleteRolePolicy",
				"iam:UpdateAssumeRolePolicy",
				"iam:AttachRolePolicy",
				"apprunner:DescribeService",
				"apprunner:ListTagsForResource",
				"apprunner:DeleteService",
				"apprunner:CreateAutoScalingConfiguration",
				"apprunner:DescribeAutoScalingConfiguration",
				"apprunner:UpdateService",
				"apprunner:DeleteAutoScalingConfiguration",
				"iam:DetachRolePolicy"
			],
			"Resource": "*"
		}
	]
}

作成したポリシーを IAM ユーザーにアタッチします。
IAM -> Users -> (作成したユーザー) -> Add permissions -> Attach policies directly から先ほど作成したポリシーをアタッチします。

ECR の作成とイメージのアップロード

適当に terraform ディレクトリと main.tf ファイルを作成して(terraform/main.tf)、以下のように記述します。

provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_ecr_repository" "hoge_api_repo" {
  name = "hoge-api-repo"
}

resource "aws_iam_role" "apprunner_role" {
  name = "apprunner-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = ["apprunner.amazonaws.com", "build.apprunner.amazonaws.com"]
        },
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "apprunner_service_ecr" {
  role       = aws_iam_role.apprunner_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess"
}

resource "aws_iam_role" "apprunner_instance_role" {
  name = "apprunner-instance-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = "tasks.apprunner.amazonaws.com"
        },
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy" "apprunner_policy" {
  name = "apprunner-policy"
  role = aws_iam_role.apprunner_role.id

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = [
          "ecr:GetDownloadUrlForLayer",
          "ecr:BatchGetImage",
          "ecr:BatchCheckLayerAvailability",
          "apprunner:CreateService",
          "apprunner:DeleteService",
          "apprunner:DescribeService",
          "apprunner:ListServices",
          "apprunner:UpdateService",
          "iam:CreateServiceLinkedRole",
          "ecr:GetAuthorizationToken",
        ],
        Resource = "*"
      }
    ]
  })
}

後は terraform ディレクトリで以下のコマンドを実行すれば、AWS に ECR リポジトリが作成されます。
ブラウザで AWS にログインし、ECR にアクセスすると hoge-api-repo が作成されていることが確認できます。
次の手順で利用するので、hoge-api-repo の URI を控えておきます。

$ cd terraform
$ terraform init && terraform plan
$ terraform init && terraform apply

イメージを保管する場所は作成できたので、ローカルにある Docker ファイルなどからコンテナイメージを作成し、ECR にアップロードします。
以下のコマンドの fuga 部分を先ほど控えた URI に変更して実行します。

$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin fuga.dkr.ecr.ap-northeast-1.amazonaws.com/hoge-api-repo
$ docker buildx build --platform linux/amd64 -t fuga.dkr.ecr.ap-northeast-1.amazonaws.com/hoge-api-repo:latest .
$ docker push fuga.dkr.ecr.ap-northeast-1.amazonaws.com/hoge-api-repo:latest

App Runner の作成

ECR にコンテナイメージをアップロードできたので、ECR にある最後にアップロードされたコンテナイメージを実行するサーバーである App Runner を作成します。
App Runner を作成する指示を main.tf(terraform/main.tf)に以下を追記します。

resource "aws_apprunner_auto_scaling_configuration_version" "hoge_api_apprunner_autoscaling" {
  auto_scaling_configuration_name = "hoge-api-autoscaling"

  max_concurrency = 30
  max_size        = 1
  min_size        = 1
}

resource "aws_apprunner_service" "hoge_api_apprunner" {
  service_name = "hoge-api-apprunner"
  auto_scaling_configuration_arn = aws_apprunner_auto_scaling_configuration_version.hoge_api_apprunner_autoscaling.arn
  depends_on   = [time_sleep.wait_10_seconds]

  source_configuration {
    authentication_configuration {
      access_role_arn = aws_iam_role.apprunner_role.arn
    }

    image_repository {
      image_identifier      = "${aws_ecr_repository.hoge_api_repo.repository_url}:latest"
      image_repository_type = "ECR"
      image_configuration {
        port = "80"
        runtime_environment_variables = {
          ENV               = "production"
          POSTGRES_PORT     = "5432"
          POSTGRES_DB_NAME  = "hoge"
        }
      }
    }
  }

  network_configuration {
    egress_configuration {
      egress_type = "DEFAULT"
    }
  }

  instance_configuration {
    instance_role_arn = aws_iam_role.apprunner_instance_role.arn
    cpu = "1024"
    memory = "2048"
  }
}

以下のコマンドを実行すると App Runner が作成されると思います。
ブラウザで AWS にログインし、App Runner にアクセスすると hoge-api-apprunner が作成されていることが確認できます。
上記の設定であれば、外からアクセスできる部分に App Runner が作成されるので、ブラウザに表示されている URL にアクセスするとコンテナイメージ内のアプリケーションが動作していることが確認できると思います。

なぜここで心が折れたのか?

EC2 の t2.micro より安くすることができなかったからです...笑
最小インスタンス数を 0 にして、アクセスがあった際に起動し、アクセスが一定数なければ落とすような運用をしたいなと想像していました。
ですが、最小インスタンス数が 1 以上でないと起動できないことを Terraform を書いているときに知りました...

インスタンスの最小構成は CPU256, Memory512 なのですが、これだと自分の場合はアプリケーションが起動できず App Runner の作成ができませんでした。

EC2 の t2.micro は 1,500 円ほどで運用できているのですが、App Runner の CPU1024/Memory2048 は 8,000 円ほどかかるので、コストが跳ね上がってしまって移行する理由が全くなくなってしましました。

App Runner 自体はフルマネージドサービスなので高いことは知っていたのですが、ここまで跳ね上がってしまうとは思ってもいませんでした...

が移行を断念した一番の原因です笑

Twitterフォロー待ってます!