AWS App Runnerとは Link to this heading

サービスの概要 Link to this heading

AppRunnerはAWSのコンピュート系の新サービスで、ソースコードのリポジトリ又は コンテナイメージからWebアプリを簡単にデプロイすることができます。

AWSでコンテナを運用する場合、これまではECSとEKSしか選択肢が無く、それなりに複雑なリソースをセットアップする必要があったんですが、 AppRunnerを利用する事でネットワーク・ロードバランサー・TLS証明書といった関連リソースを一括でデプロイ可能になります。 なおかつ同時リクエスト数に応じてオートスケールされるのでかなりの手間要らずです。

ただし、ソースコードのリポジトリからアプリをデプロイする場合、今のところランタイムはpython3とnode12のみの対応で、 これ以外のラインタイムを利用したい場合はDockerのコンテナレジストリからデプロイする必要があります。 他のサービスの傾向から察するに、今後対応ランタイムは増えていきそうなので、このあたりはもっと使いやすくなると思います。

サービスの改善要望 Link to this heading

ただ、まだリリースしたてのサービスなので気に入らない/改善して欲しいポイントはちょくちょくあるので、 このあたりはぜひAWSに対応してもらいたい所存です。

あげるとキリがないんですが、特に気なっているのは以下です。

(注意) この情報は2021/6時点のAppRunnerの仕様についての要望です。将来的には改善が進み課題は解消されると思いますので、 その前提で読んでください。

  • プライベートVPCとの通信ができず、DBをパブリックなVPCに配置せざるをえないので運用しづらい
  • WAFのインテグレーションができない
  • サイドカーでログを転送するみたいな事はできない(CloudWatchLogsにはログは吐かれるが、費用的にお高くなりそう)
  • (例えばNewRelicの様なAPMの有効化等の)特定のプロセス(Pod?Task?)にのみ環境変数や設定を変える仕組みがない
  • 設定できるCPU/Memoryのバリエーションが少ない
  • (RailsのSidekiqなどの)バックグラウンドで動かすバッチやタスクスケジューラーに適用できない

WAFやログ周りの転送、プライベートVPCとの通信については、運用上欲しい機能なので辛いところです。 またある程度の規模のアプリだと非同期処理、バッチ処理の機構を備えている物が多いんですが、 AppRunnerはHTTP通信を処理するのに特化している為、現状だとAppRunner以外のサービスにこれらを乗せる必要がありそうです。

AppRunnerのロードマップを見る感じ、 大体の課題感は認知されているようなので今後の改善を待ちましょう。

まだまだ改善ポイントが多いものの、そこそこの数のアプリ/サービスを運用・管理する身としては、うまく利用する事で運用負荷軽減がかなり見込めそうで期待大です。 GCPのCloudRunの有望な対抗馬でしょう。

セットアップ Link to this heading

Hello,World!を動かすだけならWORKSHOPの通りにやれば簡単にできるのと、 動かしてみた系の記事が既にいくつかあるので、このpostではもう少し現実に近いケースで試してみました。

実行するアプリ Link to this heading

RailsアプリをAppRunnerで動かしてみるケースを想定して動かしてみました。 また、バックエンドはAurora/MySQLを利用しています。 2021/6時点ではAppRunnerはRubyランタイムに対応していないのでDockerイメージをECRにpushして、レジストリからデプロイを行う方式で進めます。

AppRunnerの実行に必要な準備は以下です。

  • Railsアプリの作成
  • 関連AWSリソースの作成
  • RailsアプリのDockerイメージの作成
  • AppRunnerサービスの作成

Railsアプリの作成 Link to this heading

ソースコードはこちらにupしました。 単純なRailsアプリで以下の機能を持ちます。

  • /info/cpuで実行環境のCPU情報を出力
  • /info/memで実行環境のメモリ情報を出力
  • /usersでusersテーブルに対するCRUD処理

関連AWSリソースの作成 Link to this heading

必要なAWSリソースはterraformで作成しています。 ソースコードはこちらです。

ECR Link to this heading

Rubyランタイムで動すので、コンテナイレジストリが必要です。 今のところECRのみ対応しているようですので、ECRのリソースを作成します。

terraform
1# terraform
2resource "aws_ecr_repository" "blog_sample_app_runner" {
3  name                 = "blog-sample-app-runner"
4  image_tag_mutability = "MUTABLE"
5
6  image_scanning_configuration {
7    scan_on_push = true
8  }
9}

RDS/Aurora Link to this heading

AppRunnerからRDSを参照する際、DBのインスタンスはPublicなサブネット内に配置する必要があります。 AppRunnerが実行するコンテナはAWSの内部的なネットワーク内で動作する為、 直接Privateなサブネットに通信をかける事ができず、 その為、DBインスタンスをPublicなサブネット内に配置せざるを得ないのが現状です。

これはつまり、DBをインターネットに晒す必要があるという事です。 セキュリティグループ/FireWallがあるとはいえ、セキュリティ的に懸念が大きいです。

この問題は(少なくとも私にとっては)かなり致命的で、プロダクション導入の最大の障壁だと感じました。 ロードマップのissueにもかなりのリアクションがあるので、 AWS側の対応を待ちましょう。

ちなみにですが、Aurora Serverlessは仕様上、 VPC外からアクセスすることはできません。 ですのでAppRunnerからは利用できませんでした、残念。

terraform
 1# terraform
 2resource "aws_rds_cluster" "blog_sample_db" {
 3  cluster_identifier      = "blog-sample-db"
 4  engine                  = "aurora-mysql"
 5  engine_version          = "5.7.mysql_aurora.2.07.1"
 6  availability_zones      = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
 7  database_name           = "blog_sample"
 8  master_username         = "admin"
 9  master_password         = "password"
10  backup_retention_period = 1
11  preferred_backup_window = "07:00-09:00"
12  port                    = 3306
13  skip_final_snapshot     = true
14  db_subnet_group_name    = "ohr486base-public"      # SET YOUR DB SUBNET NAME
15  vpc_security_group_ids  = ["sg-0b8cb29c69dc394e6"] # SET YOUR VPC SECURITY GROUP IDS
16
17  tags = {
18    Name = "blog-sample"
19  }
20}
21
22resource "aws_rds_cluster_instance" "blog_sample_db1" {
23  identifier               = "blog-sample-db-1"
24  instance_class           = "db.t3.small"
25  cluster_identifier       = aws_rds_cluster.blog_sample_db.id
26  engine                   = aws_rds_cluster.blog_sample_db.engine
27  engine_version           = aws_rds_cluster.blog_sample_db.engine_version
28  db_subnet_group_name     = aws_rds_cluster.blog_sample_db.db_subnet_group_name
29
30  tags = {
31    Name = "blog-sample-db-1"
32  }
33}

AppRunner実行の為のIAMRole Link to this heading

terraform
 1# terraform
 2resource "aws_iam_role" "blog_sample_app_runner" {
 3  name = "blog-sample-app-runner"
 4
 5  assume_role_policy = <<EOF
 6{
 7  "Version": "2012-10-17",
 8  "Statement": [
 9    {
10      "Action": "sts:AssumeRole",
11      "Principal": {
12        "Service": [
13          "build.apprunner.amazonaws.com",
14          "tasks.apprunner.amazonaws.com"
15        ]
16      },
17      "Effect": "Allow",
18      "Sid": ""
19    }
20  ]
21}
22EOF
23}
24
25resource "aws_iam_role_policy_attachment" "blog_sample_app_runner" {
26  role       = aws_iam_role.blog_sample_app_runner.name
27  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess"
28}

RailsアプリのDockerイメージの作成 Link to this heading

RailsアプリのDockerイメージを作成する際、気になったポイントとしては以下です。

  • REPL
  • プロセス設計
  • Migration

REPL Link to this heading

Railsアプリを運用する際、運用中の(サーバー|Pod|コンテナ)にアクセスして対話型のRailsコンソールを立ち上げてデバッグする事はよくあります。 ECS Execの様に、 実行中のコンテナに対してdocker execする手段がないのでこのあたりはAWSに是非対応してもらいたい所です。

プロセス設計 Link to this heading

Railsはアプリケーションサーバーしか提供しないので、nginxなりApacheなりのwebサーバーが必要になります。 開発環境程度ならwebサーバー無しでも問題ないんですが、本番環境での運用時には以下の様な事をしたいケースがあります。

  • リクエストのバッファリング
  • 静的コンテンツのトラフィックを直接返却
  • 静的リダイレクト

また特定のログをS3等に保存したい場合、転送の為にfluentdやfluentbitといったエクスポーターも同様に必要になります。

AppRunnerの構造上、Rails以外のプロセスも1コンテナに同居させる必要がある為、 wrapperスクリプトを作成してその中で複数のプロセスを起動・スーパバイズし、 このスクリプトをコンテナのメインプロセスとして実行させる必要があります。

ただしこの方法は1コンテナ1アプリパッケージ のベストプラクティスに反するもので、1つのコンテナで多くの事をやりすぎている為、 システムが複雑になりすぎる可能性があります。 サイドカーパターンを実現する仕組みがAppRunnerに追加されるのを待ちましょう。

なおソースコードからAppRunnerを起動させる場合は、ベースになっているランタイムのAmazonLinuxのDockerイメージ に対して、起動時のコマンドの中でnginxやfluentdといった必要なミドルウェアをインストールする必要があります。 頑張ればやれなくはないかもしれませんが、素直に必要なミドルウェアをインストールしたDockerイメージをECRにpushして使う方が良いでしょう。

Migration Link to this heading

Railsでアプリを運用する際、DBのマイグレーションをどう実行するかは悩みどころです。 EC2であればcapistranoで特定のサーバーをmigratorとして実行させることが可能ですし、 EKSであればJobを利用すれば1度だけマイグレーションを実行させる事が可能です。 ECSの場合はJobに相当する機構が無いのでecs-cliからタスクを起動してoneshotでマイグレーションを実行したり、 CodeBuildを利用してマイグレーションを実行する方法が考えられます。

AppRunnerの場合、特定のコンテナを起動する術が無いので、CodeBuildやマイグレーションの為のEC2を用意して AppRunner外から実行させるしか方法がありません。

そもそもAppRunnerのメリットは必要なリソースを一括自動でセットアップしてくれる点なので、 マイグレーション用のリソースを別途利用するのは本末転倒な気がします。 このあたりもAWSの改善を待ちましょう。

今回作成するデモでは、Dockerの起動スクリプトにマイグレーション処理を入れて対応しました。 この方法は、タイミングによっては同時に複数のマイグレーションが走る可能性があるので、 本番環境では適用できないので注意してください。

Dockerfile Link to this heading

最終的なDockerfileは以下となりました。

Dockerfile
 1# Dockerfile
 2FROM ruby:3.0.0
 3
 4RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
 5    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
 6    && apt-get update -qq \
 7    && apt-get install -y nodejs yarn \
 8    && mkdir /app
 9WORKDIR /app
10COPY Gemfile /app/Gemfile
11COPY Gemfile.lock /app/Gemfile.lock
12RUN bundle install
13COPY . /app
14
15COPY entrypoint.sh /usr/bin/
16RUN chmod +x /usr/bin/entrypoint.sh
17ENTRYPOINT ["entrypoint.sh"]
18
19# for rails & mysql process
20EXPOSE 3000 3306
21
22CMD ["entrypoint.sh"]

entrypoint.shは以下です。

bash
1#!/bin/bash
2set -e
3rm -rf tmp/*
4
5# タイミングによっては同時に複数のマイグレーションが走る可能性があるので注意
6bundle exec rake db:create
7bundle exec rake db:migrate
8
9bundle exec rails server -b 0.0.0.0

AppRunnerサービスの作成 Link to this heading

必要な情報がそろったのでAppRunnerサービスを作成します。 注意点として、サービス設定で指定するIPポートをDockerfileでEXPOSEしたIPポートに合わせるようにしてください。

terraform
 1# terraform
 2resource "aws_apprunner_service" "blog_sample_app_runner" {
 3  service_name = "blog-sample-app-runner"
 4  source_configuration {
 5    image_repository {
 6      image_configuration {
 7        port = "3000" # DockerfileのEXPOSEに合わせる
 8      }
 9      image_identifier      = "${aws_ecr_repository.blog_sample_app_runner.repository_url}:latest"
10      image_repository_type = "ECR"
11    }
12  }
13  instance_configuration {
14    cpu    = 1024 # 1024|2048|(1|2) vCPU
15    memory = 2048 # 2048|3072|4096|(2|3|4) GB
16  }
17}

参考情報 Link to this heading