Rails on App Runner - マネージドランタイムで動かしてみた編 -

Posted on
Rails AWS App Runner

この記事は AWS App Runner Advent Calendar 2022 の 16 日目の記事です。

2022 年 10 月に AWS App Runner がサポートするマネージドランタイムとして複数のランタイムが追加されました。この記事では Ruby 3.1 のサポートに着目し、マネージドランタイムを利用して Ruby on Rails で開発されたアプリケーションを動かすことができるのか、検証してみようと思います。

アーキテクチャ

今回の検証で構成するアーキテクチャは以下のとおりです。

Rails on App Runner

ただ単に Rails を動かすだけだとあまりおもしろくないので、自前の VPC に構築した Aurora MySQL にデータを保存するアプリケーションをデプロイしてみます。App Runner を使用する場合、VPC Connector を利用することで、App Runner VPC と自前の VPC を接続できます。これにより App Runner サービスから 自前の VPC 内に構築した Aurora MySQL に接続できるようになります。またアプリケーションへのアクセスはパブリックアクセス(インターネット経由でのアクセス)とします。ちなみに App Runner は 2022 年 11 月にAWS App Runner プライベートサービスをリリースしているため、自前の VPC 内からプライベートな経路で App Runner サービスにアクセスする、といった構成も実現可能です。

今回の検証で利用する各種ソフトウェアのバージョンは以下のとおりです。

  • Ruby: 3.1
  • Ruby on Rails: 6.1.7
  • Aurora MySQL: 8.0.mysql_aurora.3.02.0

検証手順

大きく以下の流れで検証をすすめました。

  1. アプリケーションを GitHub リポジトリに追加
  2. AWS リソースの作成
  3. データベース初期化
  4. 動作確認

1. アプリケーションを GitHub リポジトリに追加

今回の検証の目的は「Ruby on Rails をマネージドランタイムを使って動作させること」です。そのため、GitHub リポジトリにアプリケーションのソースコードを追加しておく必要があります。アプリケーションを作成 (rails new) して GitHub リポジトリに追加します。今回は検証のため「タスクを追加・削除できる簡単な ToDo アプリケーション」を用意しました。タスクをブラウザから登録すると、データベース (今回のアーキテクチャでは Aurora MySQL) に永続化されます。Rails は config/database.yml という設定ファイルでデータベースへの接続情報を管理します。以下のとおり、Aurora MySQL への接続情報を環境変数経由で渡せるよう定義しておきます。

  <<: *default
  host: <%= ENV['DB_HOST'] %>
  database: ruby_on_rails_on_apprunner_sample_production
  username: <%= ENV['DB_USERNAME'] %>
  password: <%= ENV['DB_PASSWORD'] %>
  port: <%= ENV['DB_PORT'] %>

2. AWS リソースの作成

次に AWS のリソースを作成していきます。今回の検証ではコンソールではなく Terraform を使って構築しました。まずはじめに、App Runner 以外の AWS リソースとして、ネットワークおよびデータベース関連のリソースを作成します。具体的に作成するリソースは以下のとおりです。(これらのリソースの詳細については本記事では割愛します)

  • VPC
  • サブネット
  • インターネットゲートウェイ
  • NATゲートウェイ
  • ルートテーブル
  • Aurora MySQL

次にセキュリティグループを作成します。先程も述べたとおり、今回の検証では App Runner から自前の VPC 内の Aurora MySQL に接続します。App Runner で起動されるサービスは App Runner の VPC で起動し、自前の VPC とは VPC Connector によって接続されます。VPC Connector を作成する際にはサブネットとセキュリティグループを指定する必要があるため、VPC Connector 用のセキュリティグループを作成します。それに加えて、Aurora インスタンスにアタッチするセキュリティグループも作成する必要があります。この 2 つのセキュリティグループに対して データベースへの接続ポート (MySQL のデフォルトは 3306) で Outboud/Inbound ルールを定義します。

# VPC Connector
resource "aws_security_group" "gotodo_rails_vpc_connector" {
  name        = "${local.project_name}-${local.service_name}-vpc-connector"
  description = "VPC Connector for ${local.service_name}"
  vpc_id      = aws_vpc.rails_apprunner.id

  tags = {
    Name = "${local.project_name}-${local.service_name}-vpc-connector"
  }
}

resource "aws_security_group_rule" "gotodo_rails_vpc_connector_egress_mysql_to_vpc_cidr" {
  description       = "From: VPC Connector for App Runner To: VPC Cidr Block"
  security_group_id = aws_security_group.gotodo_rails_vpc_connector.id
  type              = "egress"
  from_port         = 3306
  to_port           = 3306
  protocol          = "tcp"
  source_security_group_id = aws_security_group.gotodo_rails_rds.id
}

# Aurora MySQL
resource "aws_security_group" "gotodo_rails_rds" {
  name        = "${local.service_name}-rds"
  description = "RDS for ${local.service_name}"
  vpc_id      = aws_vpc.rails_apprunner.id

  tags = {
    Name = "${local.service_name}-rds"
  }
}

resource "aws_security_group_rule" "gotodo_rails_rds_ingress_mysql_from_vpc_gotodo_rails_vpc_connector" {
  description              = "From: VPC Connector for App Runner To: Aurora MySQL"
  security_group_id        = aws_security_group.gotodo_rails_rds.id
  type                     = "ingress"
  from_port                = 3306
  to_port                  = 3306
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.gotodo_rails_vpc_connector.id
}

次に App Runner サービスを作成します。Terraform で作成する際には aws_apprunner_service というリソースを使用します。またマネージドランタイムを使用する場合は、GitHub リポジトリとの接続のため GitHub connections を作成する必要があり、Terraformで作成する場合は aws_apprunner_connection というリソースを使用します。なお公式ドキュメントに記載があるとおり、リソース作成後に App Runner コンソールから authentication handshake のプロセスを踏む必要があります。

configuration_source = “REPOSITORY” と設定することで、リポジトリのルートに配置された apprunner.yaml を使用して App Runner サービスを構成できます。

resource "aws_apprunner_service" "gotodo_rails" {
  service_name = local.service_name

  source_configuration {
    authentication_configuration {
      connection_arn = aws_apprunner_connection.gotodo_rails.arn
    }
    code_repository {
      code_configuration {
        configuration_source = "REPOSITORY"
      }
      repository_url = "https://github.com/kennygt51/gotodo_rails"
      source_code_version {
        type  = "BRANCH"
        value = "main"
      }
    }
  }

  network_configuration {
    ingress_configuration {
      is_publicly_accessible = true
    }

    egress_configuration {
      egress_type       = "VPC"
      vpc_connector_arn = aws_apprunner_vpc_connector.gotodo_rails.arn
    }
  }

  tags = {
    Name = local.service_name
  }
}

# Note: After creation, you must complete the authentication handshake using the App Runner console.
# ref: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apprunner_connection
resource "aws_apprunner_connection" "gotodo_rails" {
  connection_name = local.service_name
  provider_type   = "GITHUB"

  tags = {
    Name = local.service_name
  }
}

3. データベース初期化

このままデプロイしても、Aurora MySQL に データベースやテーブル、接続するユーザーが存在しないため、アプリケーションは正しく動作しません。アプリケーションを動作させるため、データベースの初期化作業をおこないます。今回の検証では Cloud9 を 自前の VPC 内に立てて、そこから mysql クライアントで Aurora MySQL に接続し、以下の作業をおこないました。

# Auroraに接続
mysql -h gotodo-rails-1.XXXX.ap-northeast-1.rds.amazonaws.com -u root -p
# データベース作成
CREATE DATABASE ruby_on_rails_on_apprunner_sample_production;
# ユーザー作成
CREATE USER 'rails_apprunner_sample_user'@'%';
ALTER USER 'rails_apprunner_sample_user'@'%' identified BY 'XXXXXXXX';
# 作成したデータベースに対して、作成した管理ユーザに全権限を付与
GRANT ALL ON ruby_on_rails_on_apprunner_sample_production.* To rails_apprunner_sample_user@'%';

4. 動作確認

terraform を apply することで App Runner サービスを作成し、デプロイが完了するまで待機 (6,7 分時間がかかります) します。無事にデプロイが終了したことを確認し、App Runner のコンソールに表示されているデフォルトドメインからブラウザでアクセスしてみると、アプリケーションが動作していることが確認できました!

Application

検証の中で詰まったこと

今回の検証を進める上でいくつか詰まった点があるので、対応方法も含めて紹介しておきます。

bundle install

一番最初の検証作業の時には何も考えず Start command に bundle exec rails server -e production -p 8080 とだけ設定して App Runner サービスを作成しましたが、Gem がインストールされていない(当然)ので動きませんでした。App Runner はアプリケーションをデプロイする前にリポジトリのルートディレクトリで実行するコマンドとして Build command を定義できます。コードのコンパイルや依存するライブラリのインストールはこの Build command で定義することになります。

Rails の場合 bundle install および assets:precompile をビルドフェーズで実行しておく必要がありました。

mysql gem のインストール

Aurora MySQL をデータベースとして利用するアーキテクチャだったため mysql2 を使用する必要がありました。

gem 'mysql2', '~> 0.5'

前述の bundle install のタイミングで、mysql2 のインストールに失敗するエラーに遭遇しました

11-30-2022 09:00:56 AM [Build]   mysql2
11-30-2022 09:00:56 AM [Build] In Gemfile:
11-30-2022 09:00:56 AM [Build] An error occurred while installing mysql2 (0.5.4), and Bundler cannot continue.

調べたところ、Ruby の mysql2 gem を使用する際には native extention として MySQL クライアントをインストールしておく必要があり、App Runner のビルドフェーズで使用されている環境に MySQL クライアントが存在しないことによるエラー (Rails の開発環境をローカルに構築する際によく遭遇するエラー) のようでした。今回の検証ではワークアラウンドとしてビルドフェーズで yum -y install mysql-devel コマンドを実行し、必要なライブラリをインストールすることとしました。

これらの検証を踏まえて設定したビルドフェーズが以下です。

    build:
      - yum -y install mysql-devel
      - bundle install
      - bundle exec rails assets:precompile

データベースのマイグレーション

データベースの初期化に加えて、アプリケーションが使うスキーマを作成をする必要があります。Ridgepole などの gem も存在しますが、今回の検証ではピュアな Active Record のマイグレーション機能を使うことにしていました。検証の初期段階では アプリケーションを動かすことを優先していたためサボって省略して、SQL を直接流してテーブルを作成していました。

CREATE TABLE `tasks` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `title` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `created_at` datetime(6) NOT NULL,
  `updated_at` datetime(6) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

検証を進める中で App Runner サービス作成時のビルドフェーズで rails db:migrate を実行すればよいのでは?と思い立ったので、ビルドフェーズでデータベースのマイグレーションを実行するようにしました。

・・・しかし、残念ながら僕の設定方法が悪いのか apprunner.yaml に定義してもマイグレーションに必要な環境変数 (データベースのホスト名やポート番号など) を読み込む部分がうまく動作せず、ビルドフェーズでマイグレーションを実行することができませんでした。残念ながら時間切れとなってしまったため、ここは今後の課題とさせてください。

シークレットの管理

データベースへの接続情報など外部に漏らしてはいけないシークレットが存在します。2022 年 12 月 16 日現在、シークレットの外部管理機能は未リリースです。今回は検証のためそこまで深入りしませんでしたが、本番環境での運用を考えると何らかの方法でシークレットを適切に管理する必要があります。

apprunner.yaml

最終的に、今回の検証では以下の apprunner.yaml を使用しました。

version: 1.0
runtime: ruby31
build:
  commands:
    build:
      - yum -y install mysql-devel
      - bundle install
      - bundle exec rails assets:precompile
run:
  command: bundle exec rails server -p 8080
  network:
    port: 8080
  env:
    - name: RAILS_ENV
      value: production
    - name: DB_HOST
      value: XXXX.ap-northeast-1.rds.amazonaws.com
    - name: DB_PORT
      value: 3306
    - name: DB_USERNAME
      value: rails_apprunner_sample_user
    - name: DB_PASSWORD
      value: XXXX
    - name: RAILS_LOG_TO_STDOUT
      value: true
    - name: SECRET_KEY_BASE
      value: XXXX

まとめ:Production Ready に向けて

今回の検証ではアプリケーションをちゃんと起動させる部分で悪戦苦闘してしまったため、本番での運用を見据えた検討まではできませんでした。今後リリースされる機能などサービスの進化を継続的にキャッチアップしていきたいと思いますが、現時点で頭の中にあった Production Ready に向けての考慮事項をリストアップして、本記事の締めとさせていただきます。(時間を見つけて Dive Deep していきたい)

  • データベースのマイグレーション
    • Ridgepole などを使ってデータベースのマイグレーションをする時にどうするか
  • シードデータの投入
    • シードデータを投入・更新する時にどうするか
  • rails console
    • やっぱ使いたいよね、みんな大好き rails console
  • 非同期ジョブ
    • Sidekiq を使いたいような場合にどうするのがいいのか