Passion make things more better

Ruby on Rails / React.js / Swift / AWS / Docker

RailsをAWS Fargateにデプロイする (AWS CLI / ECS CLIを使用)

AWS CLIとECS CLIを用いてRailsをFargateにデプロイする方法について書きます。 イメージのビルドや自動デプロイの仕組みはまた別で書こうと思っています。今回は、MacOSで実行することを想定しています。

目次

  • CLIツールのインストール
  • IAMアカウントおよびロールの作成
  • アカウント設定 / ESC設定
  • ECRの作成 & Push
  • クラスタの作成
  • デプロイについて
  • スケールについて
  • DB Migration等のコマンドについて

CLIツールのインストール

ターミナルでAWSを操作できるように、CLIツールをインストールします。今回はbrewを使ってインストールします。 他の方法は以下を参照してください。

# インストール
$ brew install awscli

# 確認
$ aws --version

# インストール
$ brew install amazon-ecs-cli

# 確認
$ ecs-cli --version

IAMアカウントおよびロールの作成

ターミナルでAWSを操作するためのアカウントを作成します。 こちらは、画面上で行います。IAMのページにアクセスして行ってください。

グループの作成

初めにポリシー(アカウントがどの操作を行えるかを制限するもの)を紐づけたグループを作成します。

今回の要件で必要となるポリシーは以下です。

  • AmazonEC2ContainerRegistryFullAccess
  • AmazonEC2ContainerServiceFullAccess
  • AmazonECSTaskExecutionRolePolicy
  • CloudWatchLogsFullAccess

ユーザーの作成

続いてIAMユーザーを作成します。先ほど作ったグループを紐づけてユーザーアカウントの作成を完了させてください。完了したら、以下を必ず控えておいてください。

  • アクセスキーID
  • シークレットアクセスキー

ロールの作成

最後にロールの作成を行います。Fargateでは、ecsTaskExecutionRoleが必要となります。以下を参考にして、作成を行ってください。

Amazon ECS タスク実行 IAM ロール

アカウント設定 / ESC設定

ターミナル上で操作するための情報が出揃ったので、続いてAWSのアカウントの設定およびESCを操作するための設定を行います。

AWS CLIの設定

先ほど作成したIAMの認証情報を設定します。PROFILE_NAMEのところには、サービス等の名前を入れておくと管理しやすいと思います。

$ aws configure --profile PROFILE_NAME

AWS Access Key ID [None]: ACCESS_KEY_ID
AWS Secret Access Key [None]: SECRET_ACCESS_KEY
Default region name [None]: ap-northeast-1
Default output format [None]: json

これを実行すると、~/.aws以下にconfigcredentialの2つのファイルが作成されます。

aws configureだけを実行して設定をするパターンがありますが、それだと複数のアカウントで運用する場合に切り替えが面倒になるので、私はprofile名を設定して運用しています。

ECS CLIの設定

AWS CLIと同様に、ECS CLIの設定も行います。CLUSTER_NAMECONFIG_NAMEには、PROFILE_NAMEと同様にサービス等の名前を入れておくと管理しやすいと思います。

※もし本番環境とテスト環境を分ける場合などはそれも含めておくと管理しやすくなります。

# 認証情報の設定
$ ecs-cli configure profile --profile-name PROFILE_NAME --access-key ACCESS_KEY_ID --secret-key SECRET_ACCESS_KEY

# Fargateに関する設定
$ ecs-cli configure --cluster CLUSTER_NAME --default-launch-type FARGATE --region ap-northeast-1 --config-name CONFIG_NAME

ECRの作成 & Push

ビルドしたDockerイメージはECRにPushするようにします。 ECRでリポジトリを作成し、URLを控えてください。

docker-compose.ymlの作成

最新のECS CLIであれば、Docker ComposeのVersion3の記述を使うことができます。

以下のように作成することができます。

version: '3'
services:
  app:
    build:
      context: ./
      dockerfile: Dockerfile
    image: ECR_REPOSITORY_URL
    ports:
      - "3000:3000"
    environment:
      RAILS_ENV: production
    logging:
      driver: awslogs
      options:
        awslogs-group: SERVICE_NAME
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: PREFIX

この場合、ECS上ではbuildは無視されます。ローカルでのbuildのために定義しています。

イメージの作成 ~ ECRへのPush

以下のコマンドを実行してECRへのPushを行うことができます。

# イメージビルド
$ docker-compose -f docker-compose.yml build

# AWS ECRへのログイン
$ $(aws ecr get-login --no-include-email --region ap-northeast-1 --profile PROFILE_NAME)

# ECRへのPush
$ docker push ECR_REPOSITORY_URL:latest

クラスタの作成

VPC / Subnet / Security Groupの作成

クラスタを作成するにあたり、VPC、Subnet、Security Groupを事前に作成しておく必要があります。 クラスタを作成する際に指定をしなければ自動で作成されるのですが、自分で作成しておいた方が管理しやすいので、私は事前に作っておくようにしています。

VPCとSubnetについては、AWSネットワークルールを参考にして作ってみてください。 Fargateで配置するコンテナはPublicなSubnet(インターネットゲートウェイでインターネットとつながっている状態)に配置する必要があるので、そこだけ注意してください。

コマンド実行

クラスタを作る際には、ecs-cli upコマンドを使用します。PROFILE_NAMECONFIG_NAMEに関しては、「アカウント設定 / ESC設定」で指定したものと同じにしてください。

$ ecs-cli up \
    --security-group SECURITY_GROUP_ID \
    --vpc VPC_ID \
    --subnets SUBNET_ID_1,SUBNET_ID_2 \
    --aws-profile PROFILE_NAME \
    --ecs-profile PROFILE_NAME \
    --cluster-config CONFIG_NAME

デプロイについて

ロードバランサーとターゲットグループの作成

サービスを起動する際に、ロードバランサーまたはターゲットグループを指定する必要があるので、事前に作成しておきます。作成時の注意としては、ターゲットグループを作成する際に、「ターゲットの種類」の項目で「IP」を指定するようにしてください。

ecs-params.ymlの作成

デプロイをする際に、どのVPC, Subnetに配置するか等を指定する必要があります。それらを指定するためにecs-params.ymlというファイルを作成します。

subnet IDとsecurity Group IDを自分で作成したものに置き換えてください。

version: 1
task_definition:
  task_execution_role: ecsTaskExecutionRole
  ecs_network_mode: awsvpc
  task_size:
    mem_limit: 1.0GB
    cpu_limit: 512
run_params:
  network_configuration:
    awsvpc_configuration:
      subnets:
        - "subnet ID 1"
        - "subnet ID 2"
      security_groups:
        - "security group ID"
      assign_public_ip: ENABLED

デプロイコマンド

デプロイをするために、ecs-cli compose service upコマンドを使用します。

$ ecs-cli compose \
    --project-name PROJECT_NAME \
    --file docker-compose.yml \
    --ecs-params ecs-params.yml \
    service up \
    --target-group-arn TARGET_GROUP_ARN \
    --container-name app \
    --container-port 3000 \
    --cluster CLUSTER_NAME \
    --aws-profile PROFILE_NAME \
    --ecs-profile PROFILE_NAME \
    --cluster-config CONFIG_NAME \
    --create-log-groups \
    --force-deployment

これでクラスター内にサービスと1つのタスクが起動するようになります。 もしサービスがすでに作成されていた場合は、タスクのみが新規で作成され、立ち上がり次第古いものと入れ替わります。

スケールについて

先ほどのコマンドでデプロイを行なった場合に、タスクの数は1となります。ここからタスクの数をさらに増やしたいという場合は、スケールさせるためのコマンドを実行する必要があります。

$ ecs-cli compose \
    --project-name PROJECT_NAME \
    service scale 2 \
    --cluster CLUSTER_NAME \
    --aws-profile PROFILE_NAME \
    --ecs-profile PROFILE_NAME \
    --cluster-config CONFIG_NAME

こちらを実行するとタスクが2つに増え、自動で不足している分のタスクが起動します。

DB Migration等のコマンドについて

Docker Composeファイルの作成

Dockerは1コンテナ1プロセスが原則ですので、この状態だとrake db:migrate等のコマンドを実行することができません。なので、そういったタスクを実行するためのDocker Composeファイルを作成します。

例として、rake db:migrateを実行するためのDocker Composeファイル(db-migration.yml)を作成します。 app用のRailsイメージのコマンドを上書きすることで同じものを利用することができます。

version: '3'
services:
  app:
    image: ECR_REPOSITORY_URL
    command: ["bundle", "exec", "rails", "db:migrate"]
    environment:
      RAILS_ENV: production
    logging:
      driver: awslogs
      options:
        awslogs-group: SERVICE_NAME
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: PREFIX

コマンド

先ほど作成したdb-migration.ymlを指定して、タスクを実行します。ここで、PROJECT_NAMEの部分は通常のappとは別の名前にしてください。もし同様の名前で実行してしまった場合に、app用のタスク定義が変わってしまい、アプリケーションが起動しなくなってしまいます。

$ ecs-cli compose \
    --project-name PROJECT_NAME \
    --file db-migration.yml \
    --ecs-params ecs-params.yml \
    up \
    --cluster CLUSTER_NAME \
    --aws-profile PROFILE_NAME \
    --ecs-profile PROFILE_NAME \
    --cluster-config CONFIG_NAME

参考

AWSネットワークルール

個人メモ。

VPCについて

各環境毎(Prodiction / Stating / Development)にVPCを作成する。 /16が推奨。

10.0.0.0/16をVPCのアドレスとして指定した場合。

  • Production -> 10.0.0.0/16
  • Staging -> 10.1.0.0/16
  • Development -> 10.2.0.0/16

サブネットについて

以下の条件の掛け合わせで分割する。 /24が推奨。

  • 運営するサービス
  • Public or Private
  • Availablity Zone

10.0.0.0/16をVPCのアドレスとして指定した場合。

  • NAME Production Public A -> 10.0.1.0/24
  • NAME Production Public C -> 10.0.2.0/24
  • NAME Production Private A -> 10.0.3.0/24
  • NAME Production Private C -> 10.0.4.0/24

ルートテーブル

  • NAME Production Private -> Private通信のみのもの
  • Name Production Public -> Internet Gatewayを割り当てたもの

2つを用意してそれぞれをサブネットにアタッチする。

参考

RailsでReact.jsを使う時のミニマムの設定

RailsでReact.jsを使う時に使用するミニマムの設定を紹介します。

構成

  • Railsのプロジェクト直下にfrontというディレクトリを用意する
  • webpackでコンパイルした際に/assets/javascripts/webpack以下にエントリファイル毎に吐き出されるようにする

ライブラリのインストール

開発環境のみ入れるもの

# webpack
$ npm install -D webpack webpack-cli

# babel
$ npm install -D @babel/core @babel/preset-env @babel/preset-react 

# sass
$ npm install -D node-sass 

# loader
$ npm install -D babel-loader sass-loader css-loader file-loader style-loader url-loader 

本番環境で使用するもの

$ npm install react react-dom

webpackの設定

webpack.config.js

const path = require('path');

module.exports = [
  {
    entry: {
      'messageEdit': './src/javascripts/entries/articles/edit.js'
    },
    output: {
      path: path.resolve(__dirname, '../app/assets/javascripts/webpack'),
      filename: "[name].js"
    },
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env', '@babel/react']
            }
          }
        },
        {
          test: /\.(css|scss)$/,
          use: ['style-loader', 'css-loader?modules', 'sass-loader']
        },
        {
          test: /\.(png|jpg|svg)$/,
          loader: 'url-loader'
        }
      ]
    },
    resolve: {
      extensions: ['.js', '.css', '.scss']
    }
  }
]

package.json

以下の内容を追記する。

"scripts": {
    "watch": "webpack -w --mode development",
    "build": "webpack --mode production"
}

RailsのAssets構成(css / javascript)について

Railsでプロジェクトを作る際のassets周り(css / javascript)の構成等についてまとめました。 ※yarnを使ったパターンは、別途まとめたいと思います。

Boostrapの導入

bootstrap-sassというgemを利用します。Gemfileに以下を記述し、bundle installを行ってください。

gem 'bootstrap-sass', '~> 3.3.7'
gem 'jquery-rails'

stylesheetの構成に関して

├── application.scss
├── base.scss
├── mixin.scss
├── pages(特定のページのみに適用したいSCSSを配置するフォルダ)
│   └── sample.scss(ファイルが無いとエラーが起きるため、仮で配置しています)
├── partials(共通で使用するパーツを配置するフォルダ)
│   └── _texts.scss(テキストサイズ・色に関するCSS)
└── variables.scss

また、application.scssbase.scsspartials/_text.scssの内容は、以下を参考にしてください。

application.scss

@import "bootstrap-sprockets";
@import "bootstrap";

@import "variables";

@import "mixin";

@import "base";

@import "partials/*";
@import "pages/*";

base.scss

body {
  font-size: 14px;
  font-weight: 300;
  word-break: break-word;
  line-height: 1.6;
}

h1,h2,h3,h4,h5,h6 {
  line-height: 1.5;
  letter-spacing: 1.5px;
  margin: 0 0 20px 0;
}

h1 { font-size: 32px; }
h2 { font-size: 24px; }
h3 { font-size: 21px; }
h4, h5, h6{ font-size: 18px; }

.section {
  margin-bottom: 80px;
}

.item {
  margin-bottom: 60px;
}

.main {
  padding-top: 80px;
}

partials/_texts.scss

.text-ex-large {
  font-size: 18px;
}

.text-large {
  font-size: 16px;
}

.text-normal {
  font-size: 14px;
}

.text-small {
  font-size: 12px;
}

.text-ex-small {
  font-size: 10px;
}

.text-bold {
  font-weight: bold;
}

javascriptsの構成に関して

├── application.js
├── libs(ライブラリを配置するフォルダ)
│   └── sample.js
├── pages(特定のページのみに適用したいJSを配置するフォルダ)
│   └── sample.js.coffee
└── partials(共通で使用するパーツを配置するフォルダ)
    └── sample.js.coffee

ページ毎にjavascriptを呼び出せるように、config/initialiers/assets.rbに以下を追記してください。

Rails.application.config.assets.precompile += %w( pages/*.js )

また、application.jsの内容は以下を参考にしてください。

application.js

//= require jquery
//= require jquery_ujs
//= require bootstrap-sprockets
//= require_tree ./libs
//= require_tree ./partials
//= require_tree ./pages

RailsでActiveModel::Serializerを使ってAPIを作成する

RailsAPIを作る時の手法として、 ActiveModel::Serializerを使ったものを紹介する。

インストール

Gemfileに以下を記述し、bundle installを実行する。

gem 'active_model_serializers'

Serializer用ファイルの作成

以下のコマンドにてSerialize用のファイルを作成する。

$ bundle exec rails g serializer モデル名

今回はUserモデルに対してのSerializer用のファイルを作成する。

$ bundle exec rails g serializer User

これを実行すると、app/serializers/user_serializer.rbというファイルが生成される。

Serializerファイルの編集

以下のようにattributesで指定した値がJSONのレスポンスとして返却される。

class UserSerializer < ActiveModel::Serializer
  attributes :id,
             :first_name,
             :last_name,
             :email,
             :phone_number
end

ネストを行いたい時

関連レコードを含めたい時は、通常のModel同様リレーションを設定する。UserモデルにProfileモデルが紐付いていると仮定した場合、そのリレーションの設定は以下のようになる。

class UserSerializer < ActiveModel::Serializer
  attributes :id,
             :first_name,
             :last_name,
             :email,
             :phone_number

  has_one :profile
end

実際に使用してみる

controllerで以下のようにレスポンスをJSONで返すように記述すると、作成したSerializerのファイルがうまいこと結果を整形してくれる。

※ない場合は、modelのattributesがそのまま吐き出される

render json: @user

ネストの指定

このままでは、Serializerファイルで記述したリレーションがそのまま実行され、関連レコードがすべて取得されてしまう。そうならないために、返却時にincludeを指定する。

render json: @user, include: [:profile]

このようにすることで指定したリレーションのみ適用される。

※もし関連レコードを取得する必要がない場合には、includes: []を指定する。

参考