🏃‍♀️

App Runner で構築する Rails アプリケーション

2022/12/06に公開

この記事は MICIN Advent Calendar 2022 の6日目の記事です。
前回は木南さんの「医療ベンチャーエンジニアが新型コロナに罹って感じたこと」でした。

はじめに

MICIN エンジニアの酒井といいます。ここ最近はあまりガッツリと AWS を触る機会も少なくなっていたので、リハビリも兼ねて以前から気になっていた AWS App Runner (以下 App Runner) を今回少し触ってみることにしました。

元々、 GCP にある類似のサービスの Cloud Run をプライベートで利用する機会があり、その開発体験がとても良かったので、 App Runner も注目していました。

MICIN では、今のところプロダクトでの採用例はありません。そこで、本番プロダクトでの利用をしようとしたときに、それらに必要な機能を備えているか、どういったところが課題になりそうか、というところを少し考えながら App Runner でシステムを組んでみます。題材には、 MICIN でも利用の多い Ruby on Rails のアプリケーション(API モードでの利用を想定)を選択しました。

App Runner

App Runner は、コンテナアプリケーションを簡単にデプロイできるフルマネージドのサービスです。


公式サイトより

マネージドな環境でコンテナをデプロイできるサービスといえば、AWS には ECS Fargate があります。Fargate では、 VPC やロードバランサを自分で構築する必要もありますし、デプロイの仕組みを整えようとするとなかなか大変な部分もあります。

一方、App Runner では、そういった VPC やロードバランサの設定もいらず、ソースコードやイメージのプッシュで自動デプロイが行えます。僅かなサービス設定をすれば、あとはコードをプッシュすれば公開サービスが立ち上げられるというのは、これまでの ECS を使った体験とは少し異なり、なかなか感動します。

運用面においても優秀で、トラフィックに応じて自動でスケーリングが行われます。自分で Auto Scaling を組んでキャパシティ管理をするというのは、なかなか大変なものです。そこを面倒見てくれるというのは非常に嬉しいポイントに感じます。

こういったスケーラビリティの面から見ると AWS Lambda が近いものに感じますが、実行環境の制約は Lambda のように厳しくなく、アプリケーション開発の際に特別なフレームワークや SDK などを使う必要はありません。ローカルの Docker 環境で Rails や Node.js のアプリケーションを開発し、それをそのままデプロイすることができます。

アプリケーション開発者からすると、とても使い勝手の良さそうなサービスには見えていますが、リリースして間もない(2021年5月ごろのリリース)ということもあり、現在だと、まだいくつかの制約はあるようです。そういった点にもこの記事では注目したいと思います。

基本の想定構成

さて、今回は、サンプルの Rails アプリケーションを App Runner にデプロイするまでを行ってみます。

想定する構成は、非常にシンプルで、 Rails を App Runner 上で動かし、バックエンドのデータベースには、 Aurora Serverless v2 (PostgreSQL) を利用します。

AWS のリソース構築は、 Terraform を用いて行います。今回利用したアプリケーションのコードと Terraform のコードはこちらのリポジトリに公開していますので、合わせて参考にしてください。

https://github.com/daisaru11/aws-apprunner-sandbox

アプリケーションのデプロイ

App Runner にアプリケーションをデプロイするには、「App Runner サービス」を作成します。
( ECS では、クラスタやサービス、タスク、タスク定義など複数の概念を扱いますが、App Runner は基本概念は、「App Runner サービス」のみと言ってよく、非常にシンプルになっています。)

Terraform の定義では、このようになります。

resource "aws_apprunner_service" "app" {
  service_name = "${var.shared_prefix}-book-app"

  source_configuration {
    authentication_configuration {
      access_role_arn = aws_iam_role.app_ecr_access.arn
    }
    image_repository {
      image_configuration {
        port = 3000
      }
      image_identifier      = "${aws_ecr_repository.app.repository_url}:latest"
      image_repository_type = "ECR"
    }

    auto_deployments_enabled = true
  }
}

IAM Role の定義や ECR の定義は省略していますが、シンプルなアプリケーションであれば、このわずかの設定でデプロイをすることができます。(のちほど、ここにDB接続のための設定等を追加していきます)

App Runner では、デプロイ方法として、コードベースのデプロイイメージベースのデプロイが選択できます。前者は、 GitHub へのプッシュをトリガーに、自動でイメージのビルドとサービスへのデプロイが行われ、そちらも非常に便利そうですが、今回はイメージベースのデプロイを利用します。

auto_deployments_enabled を有効にしているため、 ECR へイメージをプッシュすると自動でデプロイが行われます。

データベース接続

次に、アプリケーションが利用するデータベースの構築とそこへの接続を試してみます。

App Runner 自体は、 VPC を意識することなく利用できます。一方、既存の AWS のサービスは、 VPC 上への構築を行うものも多く、今回利用する Aurora Serverless v2 もその一つです。それらの VPC 上のサービスへ App Runner から接続を行うには、 VPC Connector を利用します。

VPC Connector を作成し、App Runner サービスに設定することで、アプリケーションからのアウトバウンド通信は接続した VPC 内へルーティングされるようになります。

Terraform では次のように定義します。

resource "aws_apprunner_vpc_connector" "connector" {
  vpc_connector_name = "${var.shared_prefix}-connector"
  subnets            = module.vpc.private_subnets
  security_groups    = [aws_security_group.vpc_connector.id]
}

resource "aws_security_group" "vpc_connector" {
  name        = "${var.shared_prefix}-apprunner-vpc-connector"
  description = "${var.shared_prefix}-apprunner-vpc-connector"
  vpc_id      = module.vpc.vpc_id

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

VPC Connector とセキュリティグループを作成しました。今回は特に egress の制限していませんが、制限を行うこともできます。

先ほどの App Runner サービスに VPC Connector を設定します。

resource "aws_apprunner_service" "app" {
  ...
  network_configuration {
    egress_configuration {
      egress_type       = "VPC"
      vpc_connector_arn = aws_apprunner_vpc_connector.connector.arn
    }
  }

これで、 VPC 内のリソースと通信が行えるようになります。

Aurora Serverless v2 のクラスタの定義はこの記事内では、省略しますが、こちらのように定義をしました。

シークレットの扱い

ところで、 DB の準備ができたら、DBへの接続情報をアプリケーションに教えなければなりません。

DB の接続情報には、パスワードが含まれますが、そういった秘匿情報は AWS Secret Manager などにそれらを保管した上でアプリケーションで利用できるようにしたいです。(まあ、パスワードを用いず、 IAM 認証を利用するのがより適切だと思いますが…)
あるいは、 Rails の credentials にそれらを含めるにしても、マスターキーを安全にアプリケーションに渡す必要があります。

現在のところ、 App Runner の機能としては、 Secret Manager や SSM Parameter Store のデータを、アプリケーションに対して環境変数やファイルとして渡す方法はなさそうです。

アプリケーションのエントリポイントのスクリプトの中などで、自分で Secret Manager 等から情報を取得して展開する、というような方法は取れそうですが、この辺りの連携機能はやはり欲しくなりますね。

ここは、次回の宿題として、一旦検証を進めるため、環境変数に直接 DB の接続情報を渡す形にします。

resource "aws_apprunner_service" "app" {
  ...
        image_configuration {
          port = 3000
          runtime_environment_variables = {
            DATABASE_URL = "postgres://book_app:__insecure_db_password__@${aws_rds_cluster.app_db.endpoint}:${aws_rds_cluster.app_db.port}"
          }
        }

非同期ジョブの実行

Rails で実際のアプリケーションを作ろうとすると、 HTTP のリクエストを捌くだけではなく、時間がかかるジョブをキューに入れて非同期で実行したり、スケジュールを指定して実行する必要が出てくる場面も多いと思います。

そういったジョブを実行する仕組みとして、Rails では Active Job を利用するのが一般的かと思います。 MICIN では、アダプタに Delayed::Job を用いることが多いですが、いずれにせよ常時稼働しているワーカーがキューを監視して、ジョブを実行するモデルだと思います。

一方で、 App Runner は、HTTP のリクエストに応じて、必要な数のコンテナが動的に起動し、処理を実行するモデルです。スケーリング設定で MinSize/MaxSize の設定は可能なようなので、無理やりコンテナ数を固定して起こしておくことは可能かもしれませんが、できれば App Runner の効率的な実行モデルを生かしたいところです。

そこで、なんらかのジョブキューと、そのジョブキューから HTTP で App Runner へリクエストできるような仕組みがあれば良いのではないかと考えました。
こういった仕組みは様々考えられそうですが、今回は比較的シンプルな仕組みとして、 Amazon SNS の HTTP/S サブスクリプションを利用してみます。ジョブを SNS Topic に publish すると、 subscriber となっている App Runner のサービスが HTTP で通知を受け、処理を実行します。

詳細は割愛しますので、実装は下記のコード周辺を参照してください。

また、アプリケーション内のジョブ実行だけではなく、 DB マイグレーションのようなオペレーション上のタスク実行も、この仕組みの上で行えるようにしています。

注: App Runner の制限として、ドキュメント上の記述は見つけられませんでしたが、現在はリクエストのタイムアウトは 30sec で固定のようです。長時間の処理を実行する場合は注意が必要そうです。

理想的には、こういったユースケースも自前で実装するのではなく、 App Runner の機能として、例えば EventBridge などと統合されてくると、より使いやすくなってくるのかなと感じました。

モニタリング

運用に少し目を向けて、モニタリングの機能を見てみます。

ロギング

アプリケーションログの記録は、CloudWatch Logs への出力に対応しているようです。 S3 に保存したり、 DataDog などの外部のモニタリングサービスへ連携する場合は、 CloudWatch Logs と Kinesis Data Firehose を連携するのが良さそうです。

App Runner では、サイドカーを実行できず、 Firelens のようにロギングのエージェントコンテナを追加してログ収集を行うなどは難しそうです。

トレーシング

分散トレーシングの機能としては、 AWS X-Ray との連携が提供されていました。(AWS Distro for OpenTelemetry をサポートし、その送信先として X-Ray が利用できるという形)

設定も非常に簡単で、 App Runner 側の設定としては、次のようにします。

resource "aws_apprunner_observability_configuration" "app" {
  observability_configuration_name = "${var.shared_prefix}-book-app"
  trace_configuration {
    vendor = "AWSXRAY"
  }
}

resource "aws_apprunner_service" "app" {
  ...
  observability_configuration {
    observability_configuration_arn = aws_apprunner_observability_configuration.app.arn
    observability_enabled           = true
  }

また、アプリケーション側には、OpenTelemetry の gem を組み込むだけです。

Gemfile
gem 'opentelemetry-sdk'
gem 'opentelemetry-propagator-xray'
gem 'opentelemetry-exporter-otlp'
gem 'opentelemetry-instrumentation-all'
config/initializers/opentelemetry.rb
require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
require 'opentelemetry/instrumentation/all'
require 'opentelemetry/propagator/xray'

OpenTelemetry::SDK.configure do |c|
  c.service_name = 'book-app'
  c.id_generator = OpenTelemetry::Propagator::XRay::IDGenerator
  c.use_all # enables all instrumentation
end

このシンプルな設定で分散トレーシングが実現できるのは、さすがモダンなサービスに感じます。

やり残し

ここまで、私自身が気になったポイントをいくつか調べて試してみましたが、時間が足りずできなかったのは、例えば、デプロイのパイプラインをどうするか、といったところでしょうか。( DB マイグレーションの実行と、サービス更新の実行制御など)
これはいつかの宿題に…

まとめ

短い期間で触った個人的な感触としては、 App Runner は非常に良いサービスだと感じます。シンプルにコンテナをデプロイでき、自然とスケールするシステムを構築できるのは、非常に強力だと思います。

もちろん、実際、本番のプロダクトで開発・運用すると課題は出てくると思いますが、そういった面も含めて実際のプロダクトに投入してみたいサービスだと感じることできました。

明日は、小林さんの「MICINに入社して学んだソフトウェア開発のやり方」です。お楽しみに!


MICINではメンバーを大募集しています。

「とりあえず話を聞いてみたい」でも大歓迎ですので、お気軽にご応募ください!

MICIN採用ページ:https://recruit.micin.jp/

Discussion