🚀

Copilotで作成したApp Runner,Aurora Serverlessな環境でRailsを動かす

2021/08/06に公開

こんにちは。レンティオ株式会社でエンジニアをしているMasaruTechと申します。
今回は会社のテックブログとして記念すべき最初の記事を書かせていただきました💪

始めに

ちょうどプライベートでApp RunnerとAurora Serverlessな環境でRailsアプリケーションを動かしてみていました。
しかし調べていてもあまりApp RunnerとAurora Serverlessを使ってRailsアプリケーションを動かしているのを見かけなかったので、需要があるかは分かりませんがどなたかの参考になれば幸いです。
今回はRails Tutorialのアプリケーションをお借りしてCopilot(v1.8.2)で作成しています。

Copilotのmanifestファイルなども含んだソースコードはこちらになります。
https://github.com/masaru-tech/copilot-aurora-serverless-rails

今回の構成のポイント

activerecord-aurora-serverless-adapter

Aurora ServerlessにはパブリックIPアドレスを割り当てて、VPC外からのアクセスはできません。
なので同じVPC内でないとAurora Serverlessへは接続できないです。
しかし、App RunnerではVPCやサブネットといったネットワークのリソースはAWSによって隠蔽されているため、Aurora Serverlessと同じVPC内にすることは現状ではできません。
ではApp Runnerを使うならAurora Serverlessが使えないかというとData APIを利用すればApp RunnerからでもAurora Serverlessへアクセスができます。
そこでActive RecordでData APIを利用できるようにするgemがあるのではないかと調べたところ、MySQLであれば下記のgemが使えるようだったので今回はData APIを使ってやってみました。
https://github.com/customink/activerecord-aurora-serverless-adapter

ローカルでのData API

また、Data APIをローカルでも確認できないかなと調べていたところ下記のQiitaを見つけました。
https://qiita.com/speaktech/items/d71dc06cbde97c57f01c
この通りにセットアップすることでローカルでもactiverecord-aurora-serverless-adapterを利用して動作確認ができるので助かりました。

Copilot Taskを利用したdb:migrateなどの実行

App Runnerはあくまでアプリケーションを動かすための環境ですので、Rakeタスクなどのワンショットなタスクを実行するのには向いていません。
また、App RunnerにはECS ExecにあたるものがまだサポートされていないためApp Runnerに接続して任意のコマンド実行もできません。

Copilotでワンショットなタスクを実行したい時どうやるか考えてみると

  • Backend Serviceを普段はcount: 0で必要がある時count: 1にしてcopilot svc execする
  • Taskを利用する

の2つが考えられます。
copilot svc execする方はお馴染みのECS Execなので馴染みがありますが、やはりcountを都度書き換えるのは面倒です。
copilot task runが求めているものな感じがしたのですが、アプリケーションと同様の環境変数などを手動でセットするのは骨が折れそうですし追加や変更があった場合などに適用漏れが考えられます。
しかし、generate-cmdオプションを利用すれば上記の問題も解決できそうだったので今回はTaskを利用してみました。

下準備

generate-cmdに<application>/<environment>/<service or job name>を指定するとそのServiceやJobのリソースを各フラグの値としてあらかじめ入力されたコマンドを生成してくれます。
Request-Driven Web Serviceで使えればよかったのですが、下記のようにエラーになってしまうので別途Serviceを用意しておく必要があります。

Terminal
$ copilot task run --generate-cmd mysampleapp/prod/app
✘ generate task run command from service app of application mysampleapp deployed in environment prod: retrieve network configuration for service app: no ECS service found for app in environment prod

今回はBackend Serviceとしてrails-playgroundというServiceを用意しています。
少し面倒ですが、現状TaskにServiceやJobのようなmanifestファイルがないためその代わりと思えば...
また、Aurora ServerlessやS3のバケットはrails-playgroundServiceの方で管理します。
CloudFormationのOutputsをsecretsやenvironmentsに注入してくれるのでとても便利です。

task runのひな型準備

下準備ができたらgenerate-cmdオプションを指定してtaskのひな型を準備します。

Terminal
$ copilot task run --generate-cmd mysampleapp/prod/rails-playground
copilot task run \
--execution-role arn:aws:iam::123456789012:role/mysampleapp-prod-rails-playground-ExecutionRole-XXXXXXXXXXXX \
--task-role arn:aws:iam::123456789012:role/mysampleapp-prod-rails-playground-TaskRole-XXXXXXXXXXXX \
--image 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/mysampleapp/rails-playground@sha256:517c8f3e05d96689fb8ed4b6bde2066f59b798e8229b6a19335998559be7a079 \
--entrypoint "" \
--command "" \
--env-vars AURORA_RESOURCE_ARN=arn:aws:rds:ap-northeast-1:123456789012:cluster:mysampleapp-prod-rails-playground-mysqldbcluster-xxxxxxxxxxxx,COPILOT_APPLICATION_NAME=mysampleapp,COPILOT_ENVIRONMENT_NAME=prod,COPILOT_SERVICE_DISCOVERY_ENDPOINT=mysampleapp.local,COPILOT_SERVICE_NAME=rails-playground,IMAGESTORAGE_NAME=mysampleapp-prod-image-storage,LOG_LEVEL=debug,MYSQL_SECURITY_GROUP=sg-xxxxxxxxxxxxxxxxx,RAILS_ENV=production,RAILS_LOG_TO_STDOUT=true,RAILS_SERVE_STATIC_FILES=true \
--secrets AURORA_SECRETS_ARN=arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:mysqlAuroraSecret-xxxxxxxxxxxx-yyyyyy \
--subnets subnet-xxxxxxxxxxxxxxxxx,subnet-yyyyyyyyyyyyyyyyy \
--security-groups sg-xxxxxxxxxxxxxxxxx,sg-yyyyyyyyyyyyyyyyy \
--cluster arn:aws:ecs:ap-northeast-1:123456789012:cluster/mysampleapp-prod-Cluster-xxxxxxxxxxxx

ロールに追加でポリシーを追加

このままだとAuora ServerlessのData APIを実行するのに必要なポリシーとSecrets Managerを参照するのに必要なポリシーが足りないので手動でセットする必要があります。
task-roleオプションのロールに今回はとりあえず以下を設定しました。

  • AmazonRDSDataFullAccess
  • execution-roleオプションのロールにあるインラインポリシー(mysampleapp-prod-rails-playgroundSecretsPolicy)と同じインラインポリシーを追加

db:migrateの実行

それではdb:migrateを実行してみます。
commandオプションにbin/rails db:migrateを指定します。
secretsオプションにAURORA_SECRETS_ARNを指定しておくとECSがSecrets Managerから値を取得し展開してくれます。
しかし今回はactiverecord-aurora-serverless-adapter側でそれをやってほしいのでAURORA_SECRETS_ARNをenv-varsの方に移動して実行します。

Terminal
$ copilot task run \
--execution-role arn:aws:iam::123456789012:role/mysampleapp-prod-rails-playground-ExecutionRole-XXXXXXXXXXXX \
--task-role arn:aws:iam::123456789012:role/mysampleapp-prod-rails-playground-TaskRole-XXXXXXXXXXXX \
--image 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/mysampleapp/rails-playground@sha256:517c8f3e05d96689fb8ed4b6bde2066f59b798e8229b6a19335998559be7a079 \
--command "bin/rails db:migrate" \
--env-vars AURORA_RESOURCE_ARN=arn:aws:rds:ap-northeast-1:123456789012:cluster:mysampleapp-prod-rails-playground-mysqldbcluster-xxxxxxxxxxxx,COPILOT_APPLICATION_NAME=mysampleapp,COPILOT_ENVIRONMENT_NAME=prod,COPILOT_SERVICE_DISCOVERY_ENDPOINT=mysampleapp.local,COPILOT_SERVICE_NAME=rails-playground,IMAGESTORAGE_NAME=mysampleapp-prod-image-storage,LOG_LEVEL=debug,MYSQL_SECURITY_GROUP=sg-xxxxxxxxxxxxxxxxx,RAILS_ENV=production,RAILS_LOG_TO_STDOUT=true,RAILS_SERVE_STATIC_FILES=true,AURORA_SECRETS_ARN=arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:mysqlAuroraSecret-xxxxxxxxxxxx-yyyyyy \
--subnets subnet-xxxxxxxxxxxxxxxxx,subnet-yyyyyyyyyyyyyyyyy \
--security-groups sg-xxxxxxxxxxxxxxxxx,sg-yyyyyyyyyyyyyyyyy \
--cluster arn:aws:ecs:ap-northeast-1:123456789012:cluster/mysampleapp-prod-Cluster-xxxxxxxxxxxx \
--flow

・・・

copilot-task/copilot-auro D, [2021-07-26T08:04:58.734740 #1] DEBUG -- :    (189.8ms)  SET NAMES utf8mb4,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
copilot-task/copilot-auro D, [2021-07-26T08:04:58.934930 #1] DEBUG -- :    (73.7ms)  CREATE TABLE `schema_migrations` (`version` varchar(255) NOT NULL PRIMARY KEY)
copilot-task/copilot-auro D, [2021-07-26T08:04:59.073107 #1] DEBUG -- :    (116.0ms)  CREATE TABLE `ar_internal_metadata` (`key` varchar(255) NOT NULL PRIMARY KEY, `value` varchar(255), `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL)
copilot-task/copilot-auro D, [2021-07-26T08:04:59.123485 #1] DEBUG -- :    (21.0ms)  SELECT `schema_migrations`.`version` FROM `schema_migrations` ORDER BY `schema_migrations`.`version` ASC
copilot-task/copilot-auro I, [2021-07-26T08:04:59.123652 #1]  INFO -- : Migrating to CreateUsers (20200213082638)
copilot-task/copilot-auro == 20200213082638 CreateUsers: migrating ======================================
copilot-task/copilot-auro -- create_table(:users)
copilot-task/copilot-auro D, [2021-07-26T08:04:59.189263 #1] DEBUG -- :    (62.8ms)  CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, `name` varchar(255), `email` varchar(255), `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL)
copilot-task/copilot-auro    -> 0.0633s
copilot-task/copilot-auro == 20200213082638 CreateUsers: migrated (0.0634s) =============================
copilot-task/copilot-auro D, [2021-07-26T08:04:59.273210 #1] DEBUG -- :   primary::SchemaMigration Create (24.7ms)  INSERT INTO `schema_migrations` (`version`) VALUES ('20200213082638')
copilot-task/copilot-auro I, [2021-07-26T08:04:59.293254 #1]  INFO -- : Migrating to AddIndexToUsersEmail (20200213083350)
copilot-task/copilot-auro == 20200213083350 AddIndexToUsersEmail: migrating =============================
copilot-task/copilot-auro -- add_index(:users, :email, {:unique=>true})
copilot-task/copilot-auro D, [2021-07-26T08:04:59.418309 #1] DEBUG -- :    (83.6ms)  CREATE UNIQUE INDEX `index_users_on_email`  ON `users` (`email`)
copilot-task/copilot-auro    -> 0.1244s
copilot-task/copilot-auro == 20200213083350 AddIndexToUsersEmail: migrated (0.1245s) ====================
copilot-task/copilot-auro D, [2021-07-26T08:04:59.455505 #1] DEBUG -- :   primary::SchemaMigration Create (21.4ms)  INSERT INTO `schema_migrations` (`version`) VALUES ('20200213083350')
copilot-task/copilot-auro I, [2021-07-26T08:04:59.477616 #1]  INFO -- : Migrating to AddPasswordDigestToUsers (20200213083611)
copilot-task/copilot-auro == 20200213083611 AddPasswordDigestToUsers: migrating =========================
copilot-task/copilot-auro -- add_column(:users, :password_digest, :string)
・・・

無事、db:migrateできました🎉

db:migrateさえできればこちらのものです。
App RunnerのドメインにアクセスするとRailsアプリケーションがちゃんと立ち上がっていて、Data APIを通してCRUDの操作も問題なくできました。

まとめ

MySQLに限定されてしまいますが、Data APIを経由してRailsアプリケーションを動かすことは一応できました。

Allow App Runner services to talk to AWS resources in a private Amazon VPCなんていうのがIssue#1で立っているので、無理にData APIを利用するよりはApp Runnerの改善を待つ方が早いかもしれないですね。
あとはApp RunnerでもSSM Parameter StoreやSecrets Managerに対応してくれれば最高なのですが、このあたりはCopilot Pipelineを使えばもう少しマシになりそうなので次のブログのネタにしておきます。(書きました!→Copilot SecretsがApp Runnerで使えないのをどうにかする)
そんなことをしている内にきっとAWSさんが頑張ってくれるはず |ω・)チラ

採用情報

レンティオでは絶賛、エンジニアを募集しています!
https://www.wantedly.com/companies/rentio

Discussion