Edited at

AWS FargateでRuby on Railsを動かしてみる

More than 1 year has passed since last update.

本記事は個人の意見であり、所属する組織の見解とは関係ありません。

こちらはAWS Fargate Advent Calendar 2017の12/3分の記事です。

今日は、Ruby on Railsのアプリケーションをローカル環境で作成し、それをFargateにECSでデプロイするところをやってみます。なお、データベースとしてはAmazon RDSで作成したMySQLを利用します。


事前準備

以下のものをローカルにインストールしておきます。


  • AWS CLI

  • ECS CLI

  • Docker CLI (Compose含む)


ローカル環境の構築

Docker ComposeのRailsチュートリアルが秀逸なので、これを踏襲しながら実施してみます。

手順は全てMakefileにしてみました。

new:

echo "$$_gemfile" > Gemfile
touch Gemfile.lock
docker-compose run web rails new . --force --database=mysql --skip-bundle --skip-javascript
sudo chown -R $$USER:$$USER .
echo "$$_database_yml" > config/database.yml
docker-compose build
docker-compose run web rails generate controller welcome index
docker-compose run web sh -c 'sleep 10 && rake db:create'
sudo chown -R $$USER:$$USER .

run:
docker-compose up --build

define _gemfile
source 'https://rubygems.org'
gem 'rails', '~> 5'
endef
export _gemfile

define _database_yml
default: &default
adapter: mysql2
encoding: utf8
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
password:
host: db

development:
<<: *default
database: myapp_development

test:
<<: *default
database: myapp_test

production:
<<: *default
url: <%= ENV['DATABASE_URL'] %>
endef
export _database_yml

docker-compose.ymlはこんな感じにしています。

version: '2'

services:
db:
image: mysql
environment:
MYSQL_ROOT_PASSWORD: ''
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
web:
image: ${APP_NAME}
build: .
volumes:
- .:/myapp
ports:
- "8080:3000"
environment:
PORT: "3000"
depends_on:
- db

Dockerfileはこちら。Alpine Linuxベースのものを利用しているので、イメージサイズは100MB以下にできます。

FROM ruby:2.4-alpine

RUN apk add -U mariadb-client-libs tzdata
RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN apk add -U build-base ruby-dev mariadb-dev --virtual .build-deps \
&& bundle install -j 4 \
&& gem sources --clear-all \
&& apk del .build-deps \
&& rm -rf /var/cache/apk/* \
/root/.gem/ruby/2.4.0/cache/*.gem
COPY . /myapp
CMD exec bundle exec rails s -p ${PORT} -b '0.0.0.0'

これで、実行してみます。

$ APP_NAME=rails make new

echo "$_gemfile" > Gemfile
touch Gemfile.lock
docker-compose run web rails new . --force --database=mysql --skip-bundle --skip-javascript
Creating network "fargaterails_default" with the default driver
Creating fargaterails_db_1 ...
Creating fargaterails_db_1 ... done
exist
create README.md
create Rakefile
create config.ru
create .gitignore
force Gemfile

...

docker-compose run web sh -c 'sleep 10 && rake db:create'
Starting fargaterails_db_1 ... done
Created database 'myapp_development'
Created database 'myapp_test'
sudo chown -R $USER:$USER .

Railsのテンプレートでファイルが沢山作成されました。実際にローカルで動かしてみます。

$ APP_NAME=rails make run

...

web_1 | => Booting Puma
web_1 | => Rails 5.1.4 application starting in development
web_1 | => Run `rails server -h` for more startup options
web_1 | Puma starting in single mode...
web_1 | * Version 3.11.0 (ruby 2.4.2-p198), codename: Love Song
web_1 | * Min threads: 5, max threads: 5
web_1 | * Environment: development
web_1 | * Listening on tcp://0.0.0.0:3000
web_1 | Use Ctrl-C to stop

実はAWS Cloud9の環境で実行していたので、8080番ポートが簡単にレビューできます。

Screen Shot 2017-12-03 at 7.42.26 PM.png

Screen Shot 2017-12-03 at 7.46.45 PM.png

ローカル環境 = AWS Cloud9上でDockerイメージの作成が簡単にできました。


Amazon ECRにイメージをpushする

これもMakefileでやってみます。

push:

docker-compose run web rake assets:precompile
sudo chown -R $$USER:$$USER .
docker-compose build
ecs-cli push $(APP_NAME) --region $(REGION)

$ APP_NAME=rails REGION=us-east-1 make push

docker-compose run web rake assets:precompile
Starting fargaterails_db_1 ...
Starting fargaterails_db_1 ... done
Yarn executable was not detected in the system.
Download Yarn at https://yarnpkg.com/en/docs/install
I, [2017-12-03T10:50:44.944625 #1] INFO -- : Writing /myapp/public/assets/application-f0d704deea029cf000697e2c0181ec173a1b474645466ed843eb5ee7bb215794.css
I, [2017-12-03T10:50:44.945297 #1] INFO -- : Writing /myapp/public/assets/application-f0d704deea029cf000697e2c0181ec173a1b474645466ed843eb5ee7bb215794.css.gz
sudo chown -R $USER:$USER .
docker-compose build
db uses an image, skipping
Building web
Step 1/9 : FROM ruby:2.4-alpine
---> 3bc1f3c1b02b
Step 2/9 : RUN apk add -U mariadb-client-libs tzdata
---> Using cache
---> d6ee097324d4
Step 3/9 : RUN mkdir /myapp
---> Using cache
---> d2c41af86484
Step 4/9 : WORKDIR /myapp
---> Using cache
---> caf8451075c5
Step 5/9 : COPY Gemfile /myapp/Gemfile
---> Using cache
---> b21da366053c
Step 6/9 : COPY Gemfile.lock /myapp/Gemfile.lock
---> Using cache
---> 3c8725f7ec41
Step 7/9 : RUN apk add -U build-base ruby-dev mariadb-dev --virtual .build-deps && bundle install -j 4 && gem sources --clear-all && apk del .build-deps && rm -rf /var/cache/apk/* /root/.gem/ruby/2.4.0/cache/*.gem
---> Using cache
---> 85cacd65f98d
Step 8/9 : COPY . /myapp
---> aaa9fbc3ccb4
Removing intermediate container a72a8730fc5e
Step 9/9 : CMD exec bundle exec rails s -p ${PORT} -b '0.0.0.0'
---> Running in d59d3905de01
---> 2172ed5219ae
Removing intermediate container d59d3905de01
Successfully built 2172ed5219ae
ecs-cli push rails
INFO[0000] Getting AWS account ID...
INFO[0000] Tagging image image=rails repository="414111683852.dkr.ecr.us-east-1.amazonaws.com/rails" tag=
INFO[0000] Image tagged
INFO[0000] Creating repository repository=rails
INFO[0000] Repository created
INFO[0000] Pushing image repository="414111683852.dkr.ecr.us-east-1.amazonaws.com/rails" tag=
INFO[0016] Image pushed


本番環境の準備をする

以下のものを準備してください。AWS CloudFormationで構築の自動化もできるのですが、今日の本筋とは違うので省略します。


  • Default VPCのDefault Subnet、Default Security GroupのID

  • HTTPでアクセスできるSecurity Group


    • Default VPC上に作成



  • Amazon CloudWatch LogsのLog Group - 例: /ecs/rails

  • Amazon RDS for MySQLのインスタンス


    • Security GroupはDefault Security Groupを指定



  • Amazon ECSのクラス - 例: rails-cluster


    • Default VPCを利用するので、VPCの作成は不要です



次に、ECS CLIの設定であるecs-params.ymlを作成します。

version: 1

task_definition:
ecs_network_mode: awsvpc
task_execution_role: ecsTaskExecutionRole
task_size:
cpu_limit: 256
mem_limit: 512
run_params:
network_configuration:
awsvpc_configuration:
subnets:
- <Default SubnetのID 1>
- <Default SubnetのID 2>
security_groups:
- <Default Security GroupのID>
- <HTTPアクセスできるSecurity GroupのID>
assign_public_ip: ENABLED

また、本番環境に適応する環境変数を.env.productionにまとめておきます。1

RAILS_ENV=production

RAILS_LOG_TO_STDOUT=1
RAILS_SERVE_STATIC_FILES=1
SECRET_KEY_BASE=mysecretkey
DATABASE_URL=mysql2://username:password@rds.hostname/dbname


Pushしたイメージを使ってrake db:migrateをFargateで実行する

それでは、本題のFargate上でRailsを動かしてみましょう!まずは管理タスクであるrake db:migrateを実行して、MySQL上にスキーマを作成します。ecs-cliではRunTask時のCommandの上書きができないので、migrate用のComposeファイルmigrate-compose.ymlを作成します。

version: '2'

services:
web:
image: ${IMAGE}@${DIGEST}
command: ["rake", "db:migrate"]
env_file: .env.production
logging:
driver: awslogs
options:
awslogs-region: ${REGION}
awslogs-group: ${LOG_GROUP}
awslogs-stream-prefix: ecs

以下のMakefileで実行してみましょう。

export IMAGE  = $(shell aws ecr describe-repositories --region $(REGION) --repository-names $(APP_NAME) --query 'repositories[0].repositoryUri' --output text)

export DIGEST = $(shell aws ecr batch-get-image --region $(REGION) --repository-name $(APP_NAME) --image-ids imageTag=latest --query 'images[0].imageId.imageDigest' --output text)

migrate:
ecs-cli compose -f migrate-compose.yml --project-name $(APP_NAME)-migrate up --cluster $(CLUSTER) --region $(REGION)

$ APP_NAME=rails REGION=us-east-1 LOG_GROUP=/ecs/rails CLUSTER=rails-cluster make migrate

WARN[0000] Skipping unsupported YAML option... option name=networks
WARN[0000] Skipping unsupported YAML option for service... option name=networks service name=web
INFO[0000] Using ECS task definition TaskDefinition="rails-migrage:7"
INFO[0001] Starting container... container="43bc1128-7ce7-4016-a2fc-1a330230d8ff/web"
INFO[0002] Describe ECS container status container="43bc1128-7ce7-4016-a2fc-1a330230d8ff/web" desiredStatus=RUNNING lastStatus=PROVISIONING taskDefinition="rails-migrage:7"
INFO[0014] Describe ECS container status container="43bc1128-7ce7-4016-a2fc-1a330230d8ff/web" desiredStatus=RUNNING lastStatus=PROVISIONING taskDefinition="rails-migrage:7"
INFO[0026] Describe ECS container status container="43bc1128-7ce7-4016-a2fc-1a330230d8ff/web" desiredStatus=RUNNING lastStatus=PROVISIONING taskDefinition="rails-migrage:7"
INFO[0038] Describe ECS container status container="43bc1128-7ce7-4016-a2fc-1a330230d8ff/web" desiredStatus=RUNNING lastStatus=PENDING taskDefinition="rails-migrage:7"
INFO[0050] Describe ECS container status container="43bc1128-7ce7-4016-a2fc-1a330230d8ff/web" desiredStatus=RUNNING lastStatus=PENDING taskDefinition="rails-migrage:7"
INFO[0062] Describe ECS container status container="43bc1128-7ce7-4016-a2fc-1a330230d8ff/web" desiredStatus=RUNNING lastStatus=PENDING taskDefinition="rails-migrage:7"
INFO[0075] Describe ECS container status container="43bc1128-7ce7-4016-a2fc-1a330230d8ff/web" desiredStatus=RUNNING lastStatus=PENDING taskDefinition="rails-migrage:7"
INFO[0081] Started container... container="43bc1128-7ce7-4016-a2fc-1a330230d8ff/web" desiredStatus=RUNNING lastStatus=RUNNING taskDefinition="rails-migrage:7"

実行結果のログを見てみます。先程の出力のcontainerの前半部分がタスクIDになります。

$ ecs-cli logs --region us-east-1 --cluster rails-cluster --task-id 43bc1128-7ce7-4016-a2fc-1a330230d8ff

D, [2017-12-03T06:44:06.431669 #1] DEBUG -- : (0.5ms) SET NAMES utf8, @@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

D, [2017-12-03T06:44:06.447812 #1] DEBUG -- : (14.2ms) CREATE TABLE `schema_migrations` (`version` varchar(255) NOT NULL PRIMARY KEY) ENGINE=InnoDB

D, [2017-12-03T06:44:06.467175 #1] DEBUG -- : (15.7ms) CREATE TABLE `ar_internal_metadata` (`key` varchar(255) NOT NULL PRIMARY KEY, `value` varchar(255), `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL) ENGINE=InnoDB

D, [2017-12-03T06:44:06.468243 #1] DEBUG -- : (0.4ms) SELECT GET_LOCK(6939591962871035475, 0)

D, [2017-12-03T06:44:06.479960 #1] DEBUG -- : (0.5ms) SELECT `schema_migrations`.`version` FROM `schema_migrations` ORDER BY `schema_migrations`.`version` ASC

D, [2017-12-03T06:44:06.500970 #1] DEBUG -- : ActiveRecord::InternalMetadata Load (0.6ms) SELECT `ar_internal_metadata`.* FROM `ar_internal_metadata` WHERE `ar_internal_metadata`.`key` = 'environment' LIMIT 1

D, [2017-12-03T06:44:06.508998 #1] DEBUG -- : (0.5ms) BEGIN

D, [2017-12-03T06:44:06.510555 #1] DEBUG -- : SQL (0.6ms) INSERT INTO `ar_internal_metadata` (`key`, `value`, `created_at`, `updated_at`) VALUES ('environment', 'production', '2017-12-03 06:44:06', '2017-12-03 06:44:06')

D, [2017-12-03T06:44:06.513661 #1] DEBUG -- : (2.9ms) COMMIT

D, [2017-12-03T06:44:06.514243 #1] DEBUG -- : (0.5ms) SELECT RELEASE_LOCK(6939591962871035475)


同じイメージを使ってRails ServerをFargateにデプロイする

それでは最後に、Rails Serverを立ち上げてみましょう。app-compose.ymlとして以下を準備します。migrate-compose.ymlと大差はないです。

version: '2'

services:
web:
image: ${IMAGE}@${DIGEST}
ports:
- "80:80"
env_file: .env.production
environment:
PORT: "80"
logging:
driver: awslogs
options:
awslogs-region: ${REGION}
awslogs-group: ${LOG_GROUP}
awslogs-stream-prefix: ecs

以下のMakefileで実行してみます。

export IMAGE  = $(shell aws ecr describe-repositories --region $(REGION) --repository-names $(APP_NAME) --query 'repositories[0].repositoryUri' --output text)

export DIGEST = $(shell aws ecr batch-get-image --region $(REGION) --repository-name $(APP_NAME) --image-ids imageTag=latest --query 'images[0].imageId.imageDigest' --output text)

deploy:
ecs-cli compose -f app-compose.yml --project-name $(APP_NAME)-app up --cluster $(CLUSTER) --region $(REGION)

$ APP_NAME=rails REGION=us-east-1 LOG_GROUP=/ecs/rails CLUSTER=rails-cluster make deploy

WARN[0000] Skipping unsupported YAML option... option name=networksWARN[0000] Skipping unsupported YAML option for service... option name=networks service name=web
INFO[0000] Using ECS task definition TaskDefinition="rails-app:4"
INFO[0001] Starting container... container="eadcb254-36b0-472a-93af-a40038d80e6c/web"
INFO[0001] Describe ECS container status container="eadcb254-36b0-472a-93af-a40038d80e6c/web" desiredStatus=RUNNING lastStatus=PROVISIONING taskDefinition="rails-app:4"
INFO[0014] Describe ECS container status container="eadcb254-36b0-472a-93af-a40038d80e6c/web" desiredStatus=RUNNING lastStatus=PROVISIONING taskDefinition="rails-app:4"
INFO[0026] Describe ECS container status container="eadcb254-36b0-472a-93af-a40038d80e6c/web" desiredStatus=RUNNING lastStatus=PROVISIONING taskDefinition="rails-app:4"
INFO[0038] Describe ECS container status container="eadcb254-36b0-472a-93af-a40038d80e6c/web" desiredStatus=RUNNING lastStatus=PENDING taskDefinition="rails-app:4"
INFO[0050] Describe ECS container status container="eadcb254-36b0-472a-93af-a40038d80e6c/web" desiredStatus=RUNNING lastStatus=PENDING taskDefinition="rails-app:4"
INFO[0063] Describe ECS container status container="eadcb254-36b0-472a-93af-a40038d80e6c/web" desiredStatus=RUNNING lastStatus=PENDING taskDefinition="rails-app:4"
INFO[0075] Describe ECS container status container="eadcb254-36b0-472a-93af-a40038d80e6c/web" desiredStatus=RUNNING lastStatus=PENDING taskDefinition="rails-app:4"
INFO[0087] Describe ECS container status container="eadcb254-36b0-472a-93af-a40038d80e6c/web" desiredStatus=RUNNING lastStatus=PENDING taskDefinition="rails-app:4"
INFO[0099] Describe ECS container status container="eadcb254-36b0-472a-93af-a40038d80e6c/web" desiredStatus=RUNNING lastStatus=PENDING taskDefinition="rails-app:4"
INFO[0111] Started container... container="eadcb254-36b0-472a-93af-a40038d80e6c/web" desiredStatus=RUNNING lastStatus=RUNNING taskDefinition="rails-app:4"

IPアドレスを確認してブラウザでアクセスしてみます。

$ ecs-cli ps --region us-east-1 --cluster rails-cluster

Name State Ports TaskDefinition
eadcb254-36b0-472a-93af-a40038d80e6c/web RUNNING 34.202.233.73:80->80/tcp rails-app:4

Screen Shot 2017-12-03 at 9.02.30 PM.png

Screen Shot 2017-12-03 at 9.02.42 PM.png

RAILS_ENV=productionだとWelcomeページは設定しないと出ないので、これは期待通りです。/welcome/indexにアクセスしてみると正しくレンダリングされていることがわかります。

Screen Shot 2017-12-03 at 9.02.56 PM.png


まとめ

Makefileが入ってしまったのは自分の趣味の問題なのですが、各種ツールの組み合わせは皆さんのお好きなようにされると良いと思います。最も大事なことは、EC2インスタンスのことを考えたことは一度もなく、あくまでタスクとそれに紐づくリソースについてのみ考えればよかったということです。





  1. SECRET_KEY_BASEDATABASE_URLの様な秘匿な値は本来は平文に書き込むのではなく、AWS Systems Manager Parameter Store等を使って安全にやり取りするべきですが、デモ用途ということでご容赦下さい。