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

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

![](https://cdn-images-1.medium.com/max/1600/1*GSVXxgrcREUoS_vRTbajpw.png)

こちらは[AWS Fargate Advent Calendar 2017](https://qiita.com/advent-calendar/2017/aws-fargate)の12/3分の記事です。

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

## 事前準備

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

- AWS CLI
- ECS CLI
- Docker CLI (Compose含む)

## ローカル環境の構築

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

- [docker Quickstart: Compose and Rails](https://docs.docker.com/compose/rails/)

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

```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`はこんな感じにしています。

```yaml
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以下にできます。

```Dockerfile
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'
```

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

```bash
$ 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のテンプレートでファイルが沢山作成されました。実際にローカルで動かしてみます。

```bash
$ 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](https://aws.amazon.com/jp/blogs/news/aws-cloud9-cloud-developer-environments/)の環境で実行していたので、8080番ポートが簡単にレビューできます。

<img width="1280" alt="Screen Shot 2017-12-03 at 7.42.26 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/24676/70f1a957-5210-5f44-552e-b8dfe03dc900.png">

<img width="955" alt="Screen Shot 2017-12-03 at 7.46.45 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/24676/59d5917b-b97c-7ef3-90e4-3189a186db1c.png">

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

## Amazon ECRにイメージをpushする

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

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

```bash
$ 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`を作成します。

```yaml
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]

```bash
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
```

[^1]: `SECRET_KEY_BASE`や`DATABASE_URL`の様な秘匿な値は本来は平文に書き込むのではなく、[AWS Systems Manager Parameter Store](http://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-paramstore.html)等を使って安全にやり取りするべきですが、デモ用途ということでご容赦下さい。

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

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

```yaml
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で実行してみましょう。

```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)
```

```bash
$ 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`と大差はないです。

```yaml
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で実行してみます。

```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)
```

```bash
$ 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アドレスを確認してブラウザでアクセスしてみます。

```bash
$ 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
```

<img width="1280" alt="Screen Shot 2017-12-03 at 9.02.30 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/24676/5b6fa990-3bc5-b34e-4515-be21a646e200.png">

<img width="974" alt="Screen Shot 2017-12-03 at 9.02.42 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/24676/b095d863-6d85-57e9-76be-0e3c502735fb.png">

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

<img width="971" alt="Screen Shot 2017-12-03 at 9.02.56 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/24676/d615be4d-25cd-7dad-43b0-310ac66c7702.png">

# まとめ

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