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

こちらは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等を使って安全にやり取りするべきですが、デモ用途ということでご容赦下さい。