やりたいこと
Amazon ECSで動くRails本番環境を手軽に作りたい。
やりたくないこと
- ECSを動かすためのネットワーク構築やロードバランサーの設定
- AWSコンソールを利用したコンテナのデプロイ
- 複雑なCI/CDフローの構築
→こういうときにはAWS Copilot!
AWS Copilot
AWSでコンテナ化されたアプリケーションの開発、リリースを容易に行うためのコマンドラインツールです。
コマンドを叩くとCloudFormationが動き、必要なリソースの作成やデプロイを行うことができる。CI/CDパイプラインもコマンド一つで作成できます。
※Fargate起動タイプのみサポートしています
Copilotを支える概念
https://github.com/aws/copilot-cli/wiki/EnvironmentsService
ECS上で動くコンテナアプリケーションのことです。
主にECSのサービスやタスク定義と関連があります。
Environment
本番環境やステージング環境といったステージのことです。
Environmentが異なるとVPCレベルで異なります。
主に以下のリソースと関連があります。
- VPC, Subnet
- ECS Cluster
- ALB, Security Group, Internet Gateway
- Route53
Application
ServiceとEnvironmentを束ねたひとまとまりのことです。
Copilotを使ったRailsコンテナ環境の作り方
今回はNginxをかませず、直接ALBからRailsのコンテナにアクセスする仕組みを作ります。
手順概要
- ローカルで動くRailsコンテナアプリケーションを用意
- CopilotのインストールとAWS credentialsの設定
- Applicationの作成
- Environmentの作成
- RDSインスタンスの作成
- Serviceの作成
以下のコマンドでApplication、Environment、Serviceの作成を全て行ってくれますが、Serviceの作成の前にRails起動のためのデータベース(ここではRDSを利用)が必要なので、以下コマンドは使わずに一つ一つ手順を行っていきます。
$ copilot init
1. ローカルで動くRailsコンテナアプリケーションを用意
ローカルでdocker-compose up
コマンドで起動するRailsアプリケーションを作成します。
Rails on DockerのQuickstartをalpine linuxでやってみるを参考にしつつ、APIモードで作成しました。
なお、プロジェクトフォルダ名にはアンダースコアは利用しないほうが良いです。後々、copilotを利用してCI/CDパイプラインを作成する際にCloudFormationの命名規則に引っかかりエラーになります。
利用したDockerfileとdocker-compose.ymlは以下の通りです。
FROM ruby:2.7.1-alpine3.12
ENV ROOT="/myapp" \
LANG=C.UTF-8 \
TZ=Asia/Tokyo
WORKDIR ${ROOT}
RUN apk update && \
apk upgrade && \
apk add --no-cache \
tzdata \
nodejs \
mysql-dev \
mysql-client \
vim && \
apk add --virtual build-packs --no-cache \
build-base \
curl-dev \
gcc \
g++ \
libc-dev \
libxml2-dev \
linux-headers \
make
COPY Gemfile ${ROOT}
COPY Gemfile.lock ${ROOT}
RUN bundle install
RUN apk del build-packs
COPY . ${ROOT}
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
version: '3'
services:
db:
image: mysql:5.7
environment:
MYSQL_DATABASE: myapp
MYSQL_USER: root
MYSQL_ALLOW_EMPTY_PASSWORD: 1
TZ: Asia/Tokyo
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
ports:
- "3306:3306"
volumes:
- db_volume:/var/lib/mysql
web:
build: .
command: ash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
volumes:
- .:/myapp
ports:
- "3000:3000"
depends_on:
- db
volumes:
db_volume:
取り急ぎ、コンテナ起動時にデータベースの作成とマイグレーションを行うために、entrypoint.shにコマンドを記述します。
マイグレーション用のECSタスクを作成して、適宜実行するのが良いかもしれません。
#!/bin/sh
set -e
rm -f /myapp/tmp/pids/server.pid
bundle exec rails db:create
bundle exec rails db:migrate
exec "$@"
さらに.dockerignore
でmaster.keyをビルド対象から除外している場合は、ビルド対象として含めるようにコメントアウトします。
本来は、AWS Systems Managerのパラメータストアにmaster.keyの文字列を登録して、copilotから参照できるようにmanifest.yml
(後述)のsecretsを登録すべきです。
# Ignore master key for decrypting credentials and more.
# /config/master.key
またこの段階で、ALBのヘルスチェックに引っかかってコンテナの停止→起動を繰り返さないためにも、ヘルスチェック応答用のgemであるokcomputerを導入しました。
ルートアクセスに対して応答させるために以下のような記述をします。
OkComputer.mount_at = '/'
ルートアクセスして以下のような表示が返れば準備完了です。
2. CopilotのインストールとAWS credentialsの設定
Copilotのインストール
$ brew install aws/tap/copilot-cli
AWS credentialsの設定(設定の仕方は設定の基本を参照)
リソースを作成したいAWSアカウント情報をdefaultのprofileに設定しておきます。
[default]
aws_access_key_id=XXXXXXXXXXXXXXXXXXX
aws_secret_access_key=YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
3. Applicationの作成
copilotコマンドの多くは対話型で操作が進みます。
Applicationが既に存在する場合は、既存のApplicationを利用するか確認されます。
今回は「copilot-demo」という名前のApplicationを新規に作成します。
$ copilot app init
Would you like to use one of your existing applications? (Y/n)
> n
What would you like to name your application? [? for help]
> copilot-demo
しばらく経過すると以下のログが表示され、CloudFormationでRoleが作成されたことがわかります。
ローカルではApplication名が書かれた「copilot/.workspace」というファイルが作成されます。
Use existing application: No
Application name: copilot-demo
✔ Created the infrastructure to manage services under application copilot-demo.
✔ The directory copilot will hold service manifests for application copilot-demo.
Recommended follow-up actions:
- Run `copilot init` to add a new service to your application.
4. Environmentの作成
次にEnvironmentを作成します。
今回は「production」という名前で作成しました。
$ copilot env init
What is your environment's name? [? for help]
> production
Which named profile should we use to create production? [Use arrows to move, type to filter, ? for more help]
> default
しばらく経過すると以下のログが表示され、ネットワーク周りのリソース、ECSクラスタ、ALBが作成されたことがわかります。
もしApplication作成時にRoute53に指定しているドメインを指定すれば、Route53のレコードも作成されます。
CloudFormationで「EnvironmentSecurityGroup」というセキュリティグループが作成されます、後ほどRDSセキュリティグループのインバウンドグループのソースとするためにIDを保存しておきます。
What is your environment's name? production
Which named profile should we use to create production? default
✔ Created the infrastructure for the production environment.
- Virtual private cloud on 2 availability zones to hold your services [Complete]
- Virtual private cloud on 2 availability zones to hold your services [Complete]
- Internet gateway to connect the network to the internet [Complete]
- Public subnets for internet facing services [Complete]
- Private subnets for services that can't be reached from the internet [Complete]
- Routing tables for services to talk with each other [Complete]
- ECS Cluster to hold your services [Complete]
- Application load balancer to distribute traffic [Complete]
✔ Linked account XXXXXXXXXXX and region ap-northeast-1 to application copilot-demo.
✔ Created environment production in region ap-northeast-1 under application copilot-demo.
5. RDSインスタンスの作成
Service作成時にデータベースに接続できないというエラーを回避するために、このタイミングでRDSインスタンスを作成します。
Aurora Serverlessを利用していますが、何を利用しても問題ありません。
作成後は、Environment作成時に作成された「EnvironmentSecurityGroup」を、RDSセキュリティーグループのインバウンドルールのソースに設定します。
また、作成したRDSインスタンスの情報をdatabase.ymlに反映させます。
production:
<<: *default
database: myapp_production
host: copilot-demo-database.cluster-csfienv6hggj.ap-northeast-1.rds.amazonaws.com
username: admin
password: 5ohWzjXPr7w7u8OZgyBw
6. Serviceの作成
最後にServiceを作成します。
Serviceの種類として以下の2種類があります。今回は「Load Balanced Web Service」を選択し、名前は「api」としました。
- Load Balanced Web Service: インターネットから接続がある場合に選択
- Backend Service: インターネットから接続がない場合に選択
※上記の選択によって作成されるリソースに差が出る(インターネットゲートウェイ等)
$ copilot svc init
Which service type best represents your service's architecture? [Use arrows to move, type to filter, ? for more help]
> Load Balanced Web Service
Backend Service
What do you want to name this Load Balanced Web Service? [? for help]
> api
Which Dockerfile would you like to use for api? [Use arrows to move, type to filter, ? for more help]
> ./Dockerfile
しばらく経過すると以下のログが表示され、CloudFormationでECSサービスやECSタスク定義が作成されます。
ローカルでは「copilot/api/manifest.yml」というファイルが作成されます。
Service type: Load Balanced Web Service
Service name: api
Dockerfile: ./Dockerfile
✔ Wrote the manifest for service api at copilot/api/manifest.yml
Your manifest contains configurations like your container size and port (:3000).
✔ Created ECR repositories for service api.
Recommended follow-up actions:
- Update your manifest copilot/api/manifest.yml to change the defaults.
- Run `copilot svc deploy --name api --env test` to deploy your service to a test environment.
manifest.yml
Serviceの定義をするファイルです。
Dockerイメージ、コンテナの接続ポート、アクセスを許すURLパス、ECSタスクのCPU/メモリ、ECSサービス内のタスク数、環境変数、シークレット(参照できるAWS Systems Managerのパラメータ)を指定できます。
本番環境としてRailsコンテナを起動するためにRAILS_ENVを環境変数で渡します。
通常は「RAILS_ENV = development」ですが、CopilotのEnvironmentがproductionの時は「RAILS_ENV = production」という記述です。
name: api
type: Load Balanced Web Service
image:
build: ./Dockerfile
port: 3000
http:
path: '/'
cpu: 256
memory: 512
count: 1
variables:
RAILS_ENV: development
environments:
production:
variables:
RAILS_ENV: production
最後に、以下コマンドを叩き、定義したServiceをデプロイします。
以下が実行されます。
- ローカルでDockerイメージのビルド
- DockerイメージをECRへのプッシュ
- ECSタスク更新
- ECSサービス更新
$ copilot deploy
...
Successfully tagged XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/copilot-demo/api:77673f1
Login Succeeded
The push refers to repository [XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/copilot-demo/api]
cee202204539: Pushed
2bf6e538dc23: Pushed
788fae7f9c70: Pushed
0ee2627f68ac: Pushed
246acc754b43: Pushed
68a27f30bbbc: Pushed
bb97d43854fb: Pushed
9099c9ee41ff: Pushed
3d0400229c5c: Pushed
93a2bfafa84f: Pushed
54f362ba164c: Pushed
c4b1ff92c516: Pushed
446d8e2016ac: Pushed
50644c29ef5a: Pushed
77673f1: digest: sha256:40d94e7257d0657eb2a69450caa3b9f81f33c258719f57a0cafcd857aae2e123 size: 3245
✔ Deployed api, you can access it at http://copil-Publi-1R5AI7MW2C3Q3-974847059.ap-northeast-1.elb.amazonaws.com.
実行ログ行末に記載のあるURLにアクセスすると正常に表示されており、ログを見ると本番環境として動作していることが確認できました。
画面
CloudWatchログ
Database 'myapp_production' already exists
=> Booting Puma
=> Rails 6.0.3.2 application starting in production
=> Run `rails server --help` for more startup options
Puma starting in single mode...
* Version 4.3.5 (ruby 2.7.1-p83), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: production
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
まとめ
今回作成したコードは以下です。
https://github.com/ssshun/rails-copilot
AWS Copilotを利用して、AWSコンソールを利用した操作を極力減らし、手軽にRailsコンテナを本番稼働させることができました。
CI/CDパイプラインの作成はハマりどころが多かったので別途記載します。