AWS CDKでRailsアプリをECS（on Fargate）にデプロイしてみる

![ダウンロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/ae6fadbf-8824-ce87-5f22-23b06a538e43.png)

## 概要

普段はTerraformでインフラ管理をしているのですが、つい先日、興味本位でAWS CDKを使ってみました。

とりあえず手始めにRuby on Rails製のアプリをECS（on Fargate）にデプロイしてみたので、色々とメモ書きしておきます。

<font color="Red">※Railsとは？AWS CDKとは？みたいな解説はほとんど無いので、すでにある程度それぞれの概要を押さえている人向けの記事です。</font>

## 完成イメージ

![スクリーンショット 2021-05-22 2.52.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/ccff24ae-e492-da80-2626-4a6eb5965382.png)

ECS上にちょっとしたTodoアプリをデプロイします。

## 仕様

- Ruby3系
- Rails6系
- MySQL
- Nginx
- Docker
- Amazon ECS（on Fargate）
- AWS CDK

## Ruby on Rails

まず最初にデプロイ用の簡単なアプリを作っていきましょう。今回はRuby3系 × Rails6系で進めていきます。

### 各種ディレクトリ&ファイルを作成

```
$ mkdir rails6-sample-app && cd rails6-sample-app
$ mkdir nginx
$ touch nginx/Dockerfile
$ touch nginx/nginx.conf
$ touch docker-compose.yml
$ touch Dockerfile
$ touch Gemfile
$ touch Gemfile.lock
```

最終的に次のような構成になっていればOKです。

```
rails6-sample-app
└── nginx
    ├── Dockerfile
    └── nginx.conf
├── docker-compose.yml
├── Dockerfile
├── Gemfile
├── Gemfile.lock
```

```dockerfile:./nginx/Dockerfile
FROM nginx:1.15.8

RUN rm -f /etc/nginx/conf.d/*

ADD nginx.conf /etc/nginx/conf.d/myapp.conf
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
```

```conf:./nginx/nginx.conf
upstream myapp {
  server unix:///myapp/tmp/sockets/puma.sock;
}

server {
  listen 80;
  server_name localhost;

  access_log /var/log/nginx/access.log;
  error_log  /var/log/nginx/error.log;

  root /myapp/public;

  client_max_body_size 100m;
  error_page 404             /404.html;
  error_page 505 502 503 504 /500.html;
  try_files  $uri/index.html $uri @myapp;
  keepalive_timeout 5;

  location @myapp {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_pass http://myapp;
  }
}
```

```yml:./docker-compose.yml
version: "3"
services:
  web:
    build:
      context: .
    command: bundle exec puma -C config/puma.rb
    volumes:
      - .:/myapp
      - public-data:/myapp/public
      - tmp-data:/myapp/tmp
      - log-data:/myapp/log
    depends_on:
      - db
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - "3306:3306"
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - db-data:/var/lib/mysql
  nginx:
    build:
      context: nginx
    volumes:
      - public-data:/myapp/public
      - tmp-data:/myapp/tmp
    ports:
      - 80:80
    depends_on:
      - web
volumes:
  public-data:
  tmp-data:
  log-data:
  db-data:
```

```dockerfile:./Dockerfile
FROM ruby:3.0

RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt-get update -qq && apt-get install -qq --no-install-recommends \
    nodejs \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*
RUN npm install -g yarn@1

ENV APP_PATH /myapp

RUN mkdir $APP_PATH
WORKDIR $APP_PATH

ADD Gemfile $APP_PATH/Gemfile
ADD Gemfile.lock $APP_PATH/Gemfile.lock
RUN bundle install
ADD . $APP_PATH

RUN mkdir -p tmp/sockets
```

```ruby:./Gemfile
source "https://rubygems.org"
gem "rails", "~>6"
```

```ruby:./Gemfile.lock
# 空欄のままでOK
```

### rails new

おなじみの「rails new」でサクッとプロジェクトを作成します。

```
$ docker-compose run web rails new . --force --no-deps --database=mysql --skip-test --webpacker
```

Gemfileが更新されたので再度ビルド。

```
$ docker-compose build
```

### 「./config/puma.rb」を編集

```ruby:./config/puma.rb
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i
threads threads_count, threads_count
port        ENV.fetch("PORT") { 3000 }
environment ENV.fetch("RAILS_ENV") { "development" }
plugin :tmp_restart

app_root = File.expand_path("../..", __FILE__)
bind "unix://#{app_root}/tmp/sockets/puma.sock"

stdout_redirect "#{app_root}/log/puma.stdout.log", "#{app_root}/log/puma.stderr.log", true
```

### 「./config/database.yml」を編集

```yml:./config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password # デフォルトだと空欄になっているはず
  host: db # デフォルトだとlocalhostになっているはず

development:
  <<: *default
  database: myapp_development

test:
  <<: *default
  database: myapp_test

production:
  <<: *default
  database: <%= ENV["DATABASE_NAME"] %>
  username: <%= ENV["DATABASE_USERNAME"] %>
  password: <%= ENV["DATABASE_PASSWORD"] %>
  host: <%= ENV["DATABASE_HOST"] %>
```

データベースを作成。

```
$ docker-compose run web rails db:create
```


### 動作確認

コンテナを起動。

```
$ docker-compose up -d
```

![スクリーンショット 2021-05-21 23.48.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/f5f44c60-a7be-4f34-ad8e-05ddfa157441.png)

[http://localhost](http://localhost) にアクセスしていつもの画面が表示されればOK。

### Todoアプリを作成

さすがにこのままだと味気無いので、scaffoldコマンドを使って簡単なTodoアプリを作りましょう。

```
$ docker-compose run web rails g scaffold todo content:text
$ docker-compose run web rails db:migrate
```

ルーティングを設定。

```ruby:./config/routes.rb
Rails.application.routes.draw do
  root to: "todos#index"
  resources :todos
end
```

再度 [http://localhost](http://localhost) にアクセスしてください。

![スクリーンショット 2021-04-02 2.24.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/c421b49f-83e2-353a-3bf5-2b902f80c299.png)

こんな感じでTodoアプリができていれば成功です。

## AWS CDK

いよいよAWS CDKの出番です。導入方法や初期設定については他にいくらでも記事が出ているのでそちらを参照ください。

参照記事: [AWS CDKの始め方](https://qiita.com/maripoyo/items/f30ac4e8d2047c4edbe4)

なお、今回はTypeScriptで書いていくつもりです。

### ディレクトリを作成 & 初期化

インフラ部分のコードをどのように運用するかは好みによって違うと思いますが、個人的には別リポジトリに分けた方が管理しやすいと思うので、今回は分離する方向でいきます。

```
$ mkdir aws-cdk-ecs-on-fargate-sample && cd aws-cdk-ecs-on-fargate-sample
$ cdk init app --language typescript

...省略...

Applying project template app for typescript
# Welcome to your CDK TypeScript project!

This is a blank project for TypeScript development with CDK.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

## Useful commands

 * `npm run build`   compile typescript to js
 * `npm run watch`   watch for changes and compile
 * `npm run test`    perform the jest unit tests
 * `cdk deploy`      deploy this stack to your default AWS account/region
 * `cdk diff`        compare deployed stack with current state
 * `cdk synth`       emits the synthesized CloudFormation template

Initializing a new git repository...
Executing npm install...

...省略...

✅ All done!
****************************************************
*** Newer version of CDK is available [1.105.0]  ***
*** Upgrade recommended (npm install -g aws-cdk) ***
****************************************************
```

初期化までに少し時間がかかるかもしれませんが、完了した後は次のような感じの構成になっているはずです。

```
aws-cdk-ecs-on-fargate-sample
├── bin
│   └── aws-cdk-ecs-on-fargate-sample.ts
├── lib
│   └── aws-cdk-ecs-on-fargate-sample-stack.ts
├── test
│   └── aws-cdk-ecs-on-fargate-sample.test.ts
├── cdk.json
├── jest.config.js
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json
```

参照記事: [CDKアプリケーションのディレクトリ構造・処理フロー入門](https://blog.3qe.us/entry/2019/10/17/200342)

### 各種パッケージをインストール

これからの作業で必要になるパッケージがいくつかあるので、先にまとめてインストールしておきます。

```
$ npm i @aws-cdk/aws-ec2 @aws-cdk/aws-ecr @aws-cdk/aws-ecs @aws-cdk/aws-ecs-patterns @aws-cdk/aws-iam @aws-cdk/aws-logs @aws-cdk/aws-rds @aws-cdk/aws-s3 @types/uuid dotenv
```

- CDK関連
  - AWS CDKで各リソースを作成するために使用。
- uuid
  - 一意の識別子を生成するために使用。（後述のS3バケット名用）
- dotenv
  - 環境変数を取り扱うために使用。

### 各ファイルを記述

主にいじくるのは以下の2ファイルです。

- ./bin/aws-cdk-ecs-on-fargate-sample.ts
- ./lib/aws-cdk-ecs-on-fargate-sample-stack.ts

```ts:./bin/aws-cdk-ecs-on-fargate-sample.ts
#!/usr/bin/env node
import "source-map-support/register"
import * as cdk from "@aws-cdk/core"
import { AwsCdkEcsOnFargateSampleStack } from "../lib/aws-cdk-ecs-on-fargate-sample-stack"

const app = new cdk.App()
new AwsCdkEcsOnFargateSampleStack(app, "AwsCdkEcsOnFargateSampleStack", {
  env: { region: "ap-northeast-1" }
})
```

```ts:./lib/aws-cdk-ecs-on-fargate-sample-stack.ts
import * as cdk from "@aws-cdk/core"
import * as ec2 from "@aws-cdk/aws-ec2"
import * as ecs from "@aws-cdk/aws-ecs"
import * as ecs_patterns from "@aws-cdk/aws-ecs-patterns"
import * as iam from "@aws-cdk/aws-iam"
import * as ecr from "@aws-cdk/aws-ecr"
import * as s3 from "@aws-cdk/aws-s3"
import * as rds from "@aws-cdk/aws-rds"
import * as logs from "@aws-cdk/aws-logs"
import { v4 as uuid } from "uuid"
import * as dotenv from "dotenv"

dotenv.config()

export class AwsCdkEcsOnFargateSampleStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    // 各リソースの接頭語
    const resoucePrefix: string = "aws-cdk-ecs-on-fargate-sample"
    
    // ECR(App)
    const appImageRepo = new ecr.Repository(this, "appImageRepo", {
      repositoryName: `${resoucePrefix}-app`,
      imageScanOnPush: true,
      imageTagMutability: ecr.TagMutability.MUTABLE
    })

    cdk.Tags.of(appImageRepo).add("Name", `${resoucePrefix}-app-image-repo`)

    // ECR(Nginx)
    const nginxImageRepo = new ecr.Repository(this, "nginxImageRepo", {
      repositoryName: `${resoucePrefix}-nginx`,
      imageScanOnPush: true,
      imageTagMutability: ecr.TagMutability.MUTABLE
    })

    cdk.Tags.of(nginxImageRepo).add("Name", `${resoucePrefix}-nginx-image-repo`)
    
    // VPC（次の記述だけでそれに紐づいたサブネット、インターネットゲートウェイ、ルートテーブルも同時に作成される）
    const vpc = new ec2.Vpc(this, "vpc", {
      cidr: "10.0.0.0/16",
      enableDnsHostnames: true,
      enableDnsSupport: true,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: "public",
          subnetType: ec2.SubnetType.PUBLIC
        }
      ]
    })

    cdk.Tags.of(vpc).add("Name", `${resoucePrefix}-vpc`)
    
    // セキュリティグループ(ALB用)
    const albSg = new ec2.SecurityGroup(this, "albSg", {
      vpc, // 本来は「vpc: vpc」という記述が正しいが、左辺と右辺が同じ場合は省略可能
      allowAllOutbound: true
    })

    albSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80))
    albSg.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.allTraffic()) // インバウンドとアウトバウンドは必ずセット

    cdk.Tags.of(albSg).add("Name", `${resoucePrefix}-alb-Sg`)

    // セキュリティグループ(DB用)
    const dbSg = new ec2.SecurityGroup(this, "dbSg", {
      vpc,
      allowAllOutbound: true
    })

    dbSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(3306))
    dbSg.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.allTraffic())

    cdk.Tags.of(dbSg).add("Name", `${resoucePrefix}-db-Sg`)

    // データベース(RDS)
    const db = new rds.DatabaseInstance(this, "db", {
      vpc,
      vpcSubnets: {
        subnets: vpc.publicSubnets
      },
      // どのデータベース、インスタンスタイプを使うかは各自お好みで
      engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0 }),
      instanceIdentifier: `${resoucePrefix}-db`,
      instanceType:  ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
      allocatedStorage: 20,
      storageType: rds.StorageType.GP2,
      databaseName: process.env.DATABASE_NAME || "",
      credentials: {
        username: process.env.DATABASE_USERNAME || "",
        password: cdk.SecretValue.plainText(process.env.DATABASE_PASSWORD || "")
      },
      port: 3306,
      multiAz: true,
      securityGroups: [dbSg]
    })

    cdk.Tags.of(db).add("Name", `${resoucePrefix}-db`)

    // IAMロール
    const ecsTaskExecutionRole = new iam.Role(this, "ecsTaskExecutionRole", {
      roleName: "ecs-task-execution-role",
      assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonECSTaskExecutionRolePolicy"),
        iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMReadOnlyAccess")
      ]
    })

    cdk.Tags.of(ecsTaskExecutionRole).add("Name", `${resoucePrefix}-ecs-task-execution-role`)

    // クラスター
    const cluster = new ecs.Cluster(this, "cluster", {
      vpc,
      clusterName: `${resoucePrefix}-cluster`
    })

    cdk.Tags.of(cluster).add("Name", `${resoucePrefix}-cluster`)

    // ロググループ
    const logGroup = new logs.LogGroup(this, "logGroup", {
      logGroupName: "/aws/cdk/ecs/sample"
    })

    cdk.Tags.of(logGroup).add("Name", `${resoucePrefix}-log-group`)

    // タスク定義
    const taskDefinition = new ecs.FargateTaskDefinition(this, "taskDefinition", {
      family: `${resoucePrefix}-app-nginx`,
      cpu: 512,
      memoryLimitMiB: 1024,
      executionRole: ecsTaskExecutionRole,
      taskRole: ecsTaskExecutionRole
    })

    cdk.Tags.of(taskDefinition).add("Name", `${resoucePrefix}-task-definition`)

    // コンテナ定義（App）
    const appContainer = new ecs.ContainerDefinition(this, "appContainer", {
      containerName: "app",
      taskDefinition,
      // ECRからイメージを取得
      image: ecs.ContainerImage.fromEcrRepository(
        ecr.Repository.fromRepositoryName(this, "appImage", `${resoucePrefix}-app`)
      ),
      logging: ecs.LogDriver.awsLogs({
        streamPrefix: "production",
        logGroup
      }),
      // 環境変数
      environment: {
        DATABASE_HOST: db.dbInstanceEndpointAddress,
        DATABASE_NAME: process.env.DATABASE_NAME || "",
        DATABASE_PASSWORD: process.env.DATABASE_PASSWORD || "",
        DATABASE_USERNAME: process.env.DATABASE_USERNAME || "",
        RAILS_ENV: "production",
        RAILS_MASTER_KEY: process.env.RAILS_MASTER_KEY || "",
        TZ: "Japan"
      },
      command: [
        "bash",
        "-c",
        "bundle exec rails db:migrate && bundle exec rails assets:precompile && bundle exec puma -C config/puma.rb"
      ],
      workingDirectory: "/myapp",
      essential: true
    })

    // コンテナ定義（Nginx）
    const nginxContainer = new ecs.ContainerDefinition(this, "nginxContainer", {
      containerName: "nginx",
      taskDefinition,
      // ECRからイメージを取得
      image: ecs.ContainerImage.fromEcrRepository(
        ecr.Repository.fromRepositoryName(this, "nginxImage", `${resoucePrefix}-nginx`)
      ),
      logging: ecs.LogDriver.awsLogs({
        streamPrefix: "production",
        logGroup
      }),
      portMappings: [
        {
          protocol: ecs.Protocol.TCP,
          containerPort: 80
        }
      ],
      workingDirectory: "/myapp",
      essential: true
    })

    // Appコンテナをボリュームとして指定
    nginxContainer.addVolumesFrom({
      sourceContainer: "app",
      readOnly: false
    })

    // デフォルトのコンテナをNginxコンテナに指定
    taskDefinition.defaultContainer = nginxContainer

    // S3（ログ保管場所）
    const albLogsBucket = new s3.Bucket(this, `alb-logs-bucket-${uuid()}`) // バケット名は全世界においてユニークである必要があるのでuuidを使用
    cdk.Tags.of(albLogsBucket).add("Name", `${resoucePrefix}-alb-logs-bucket`)

    // サービス（次の記述だけでそれに紐づいたロードバランサーやターゲットグループが同時に作成される）
    const service = new ecs_patterns.ApplicationLoadBalancedFargateService(this, "service", {
      serviceName: `${resoucePrefix}-service`,
      cluster,
      taskDefinition,
      desiredCount: 1,
      minHealthyPercent:100,
      maxHealthyPercent: 200,
      assignPublicIp: true,
      publicLoadBalancer: true,
      securityGroups: [albSg, dbSg]
    })

    cdk.Tags.of(service).add("Name", `${resoucePrefix}-service`)
    service.loadBalancer.logAccessLogs(albLogsBucket)
  }
}
```

もしこの時点でエラーが発生している場合、「./package.json」を開いて各CDK関連のバージョンを確認してみてください。

たとえば、上記のコードで動作確認ができているのは「^1.104.0」においてです。

```json:./package.json
"dependencies": {
  "@aws-cdk/aws-ec2": "^1.104.0",
  "@aws-cdk/aws-ecr": "^1.104.0",
  "@aws-cdk/aws-ecs": "^1.104.0",
  "@aws-cdk/aws-ecs-patterns": "^1.104.0",
  "@aws-cdk/aws-iam": "^1.104.0",
  "@aws-cdk/aws-logs": "^1.104.0",
  "@aws-cdk/aws-rds": "^1.104.0",
  "@aws-cdk/aws-s3": "^1.104.0",
  "@aws-cdk/core": "1.104.0"
}
```

この辺の数字が異なる場合、正常な動作をしない可能性があるので注意しましょう。特にCDKは頻繁にアップデートが行われている事で有名なので...。

もしバージョンが異なる場合、

```
$ npm i @aws-cdk/aws-ec2@^1.104.0
```

みたいな感じでパッケージ名の後に「@バージョン」を付ける事で特定のバージョンをインストールしてください。

### 環境変数をセット

忘れないうちに環境変数をセットしておきます。

```
$ touch .env
```

```./.env
DATABASE_NAME=sample_app_production
DATABASE_PASSWORD=password
DATABASE_USERNAME=root
RAILS_MASTER_KEY=<先ほど作成したRailsアプリのmaster.key>
```

### コンパイル

JavaScriptにコンパイルします。

```
$ npm run build
```

```
aws-cdk-ecs-on-fargate-sample
├── bin
│   ├── aws-cdk-ecs-on-fargate-sample.d.ts
│   ├── aws-cdk-ecs-on-fargate-sample.js
│   └── aws-cdk-ecs-on-fargate-sample.ts
├── cdk.out
│   ├── AwsCdkEcsOnFargateSampleStack.template.json
│   ├── cdk.out
│   ├── manifest.json
│   └── tree.json
├── lib
│   ├── aws-cdk-ecs-on-fargate-sample-stack.d.ts
│   ├── aws-cdk-ecs-on-fargate-sample-stack.js
│   └── aws-cdk-ecs-on-fargate-sample-stack.ts
├── test
│   └── aws-cdk-ecs-on-fargate-sample.test.ts
├── cdk.json
├── jest.config.js
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json
```

こんな感じでjsファイルに変換されて「cdk.out」ディレクトリが新たに作られているのを確認してください。

### CloudFormationのテンプレートを作成

実際にCDKの裏側で動いているのはCloudFormationなので、次のコマンドでテンプレートを作成します。

```
$ cdk synth --profile <AWS CLIのプロファイル名>
```

すると、「./cdk.out/AwsCdkEcsOnFargateSampleStack.template.json」内にこれから作成される予定のリソース情報がズラっと書き込まれます。

```json:./cdk.out/AwsCdkEcsOnFargateSampleStack.template.json
{
  "Resources": {
    "appImageRepoCB5D21AD": {
      "Type": "AWS::ECR::Repository",
      "Properties": {
        "ImageScanningConfiguration": {
          "ScanOnPush": true
        },
        "ImageTagMutability": "MUTABLE",
        "RepositoryName": "aws-cdk-ecs-on-fargate-sample-app",
        "Tags": [
          {
            "Key": "Name",
            "Value": "aws-cdk-ecs-on-fargate-sample-app-image-repo"
          }
        ]
      },
      "UpdateReplacePolicy": "Retain",
      "DeletionPolicy": "Retain",
      "Metadata": {
        "aws:cdk:path": "AwsCdkEcsOnFargateSampleStack/appImageRepo/Resource"
      }
    },
    ...省略...
```

### デプロイ 

準備ができたのでデプロイの時です。

```
$ cdk deploy --profile <AWS CLIのプロファイル名>

Do you wish to deploy these changes (y/n)? y
AwsCdkEcsOnFargateSampleStack: deploying...
AwsCdkEcsOnFargateSampleStack: creating CloudFormation changeset...
```

デプロイには非常に時間がかかる（特にRDS作成時）ので、今のうちにECRリポジトリにAppイメージとNginxイメージをプッシュしておきましょう。

**ログイン**

```
$ aws --profile <AWS CLIのプロファイル名> ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com 
```

**App**

※「rails6-sample-app」リポジトリ配下で作業してください。

```
$ mkdir ecs
$ touch ecs/Dockerfile
```

```dockerfile:./ecs/Dockerfile
FROM ruby:3.0

RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt-get update -qq && apt-get install -qq --no-install-recommends \
    nodejs \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*
RUN npm install -g yarn@1

ENV APP_PATH /myapp

RUN mkdir $APP_PATH
WORKDIR $APP_PATH

ADD Gemfile $APP_PATH/Gemfile
ADD Gemfile.lock $APP_PATH/Gemfile.lock
RUN bundle install
ADD . $APP_PATH

# Nginxと通信を行うための準備
RUN mkdir -p tmp/sockets
RUN mkdir -p tmp/pids

VOLUME $APP_PATH/public
VOLUME $APP_PATH/tmp

RUN yarn install --check-files
RUN SECRET_KEY_BASE=placeholder bundle exec rails assets:precompile
```

本番用のDockerfile（./ecs/Dockerfile）が準備できたら、そちらを元にビルドします。

```
$ docker build -f ./ecs/Dockerfile . -t aws-cdk-ecs-on-fargate-sample-app
```

本番用のDockerイメージのビルドが終わったら、次のコマンドでECRリポジトリへプッシュしてください。

```
$ docker tag aws-cdk-ecs-on-fargate-sample-app:latest <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/aws-cdk-ecs-on-fargate-sample-app:latest
$ docker push <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/aws-cdk-ecs-on-fargate-sample-app:latest
```

この辺のコマンドは各リポジトリ内にも記載されているのでそちらを参照しても大丈夫です。

![スクリーンショット 2021-05-23 16.26.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/388e090e-ea82-683d-fdb4-a50d9060fa4c.png)

**Nginx**

※「rails6-sample-app」リポジトリ配下で作業してください。

```
$ cd nginx
$ docker build -f ./Dockerfile . -t aws-cdk-ecs-on-fargate-sample-nginx 
$ docker tag aws-cdk-ecs-on-fargate-sample-nginx:latest <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/aws-cdk-ecs-on-fargate-sample-nginx:latest
$ docker push <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/aws-cdk-ecs-on-fargate-sample-nginx:latest
```

先ほどと大体同じような要領ですね。

### 動作確認

![スクリーンショット 2021-05-22 2.03.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/b5b4cc22-1236-fab0-7b1b-60691243047a.png)

デプロイが完了したら、AWSコンソール画面から「Elastic Container Service」→「クラスター」と進み、サービス:1、実行中のタスク:1となっている事を確認してください。（もしここでダメそうならCloudWatchのログを見て適宜デバッグをお願いします。）

**App**

![スクリーンショット 2021-05-22 2.12.40_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/4caed2c0-006a-d385-c438-4a0b4e1a2510.jpeg)

**Nginx**

![スクリーンショット 2021-05-22 2.13.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/ece2e283-6274-383d-ed2e-8a5eca5d2097.png)

参考までに、AppとNginxそれぞれこんな感じでログが表示されていればOKです。


![スクリーンショット 2021-05-22 2.05.44_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/7ca8d5a5-8d4e-f1e3-0c81-f971ce9eadb7.jpeg)

あとはロードバランサーのDNS名にアクセスし、冒頭で作ったようなRailsアプリが表示されればデプロイ成功です。

![スクリーンショット 2021-05-22 2.16.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/46a3ad57-65a3-a55e-935c-9c3d96dcb75a.png)

なお、環境によってはデプロイ反映までに時間がかかる可能性もあるので注意。自分の場合はロードバランサーのDNS名にアクセスしてもタイムアウトになってしまう事があったので、少し時間を空けて再チャレンジするなどしました。

ログを見て特に問題無さそうであれば基本的には時間が解決してくれると思うので、気長に待ってみてください。

## あとがき

以上、AWS CDKを使ってECS環境を構築し、簡単なRailsアプリをデプロイしてみました。

個人的な感想としては以下の通り。

- メリット
  - プログラマブルに書ける（変数の使い回しが容易だったり、繰り返しや条件分岐なども可能）。
  - 馴染み深い言語で書ける（今回はTypeScriptで書いたが、PHPやPythonなどもある）。
  - ある程度適当でも何とかなってしまう（コード内にも記述している通り、たとえばVPCを一つ作成すればそれに紐づいた良い感じの他リソース（サブネット、インターネットゲートウェイ、ルートテーブルなど）もまとめて作成してくれるのであまり細かく考えなくても良い）。
  - TypeScriptとVSCodeを併用した場合の恩恵がデカい（必須パラメータが欠けていると即座にエラーを出してくれるため、どれが重要なのかわかりやすい。また、サジェスト機能も充実しているのでどんなパラメータが渡せるのかが一目でわかる）。
- デメリット
  - デプロイがやや遅い。
  - バージョンアップがマメすぎる（破壊的な変更も割と高頻度で行われているぽいので、長い目で見るとメンテナンスが大変かも）。
  - ところどころいじれないパラメータがある（TerraformではいじれるけどCDKでは無理そうなパラメータがいくつかあった）。
  - ある程度適当でも何とかなってしまう（これはメリットの部分で触れた事の裏返しもあるが、適当にやっても何とかなってしまう分、各リソースの本質的な理解につながらなかったり、結局何がどう作成されたのか把握できなくなってしまう可能性があるかも？）。
  - まだ情報が少ない（比較的新しめのツールなので、情報が少ない印象を受ける。特に日本語のものは現状不足している感が否めず、英語での検索を前提に動く必要がありそう）。


※主にTerraformと比べた場合の感想です。

自分としては、操作性という意味でTypeScriptとVSCodeを併用した場合の恩恵に非常に大きなメリットを感じましたが、それなりにデメリットもあるようなので、今後Terraformから乗り換えるかどうかと言われれば検討中です。

一応、どちらもある程度は使えるようにしておいて、今後の世間的な動向に合わせて適応してきたいなと思います。

**今回作成したコード**

https://github.com/kazama1209/rails6-sample-app

https://github.com/kazama1209/aws-cdk-ecs-on-fargate-sample