Rails × CircleCI × ECSのインフラ構築

簡単なDocker RailsアプリをECSを利用して本番環境に上げるまでのまとめ

＊ あくまで参考に(実務でそのまま利用できるほどしっかり構築しておりません)

![image.png](https://cacoo.com/diagrams/nEYNTgyozWzsHKqQ-7F3E1.png)

# 前提知識
ECSとは？クラスターとは？サービスとは？タスクとは？って人は
[ECSの概念を理解しよう](http://www.mpon.me/entry/2017/11/19/200000)
などを読んでください。

# Railsアプリ作成
まずはローカルでRailsアプリを作成しましょう。
機能は簡単なものでいいので、scaffoldなどを利用してサクッと作成してしまいましょう。
脳死で作成したい人は下記をご覧下さい。
[Docker Rails Sampleアプリ構築 \- Qiita](https://qiita.com/key_sn/items/6a04dd9a03012a691119)


# AWS上で利用するリソースの作成
コンソール上(or Terraformなど)からあらかじめ作成しておくべきものになります。

## IAMロール・ポリシーの作成
ECSで運用するための必要なIAMロール・ポリシーを作成していきます。
ちなみにポリシーとは、ロールに付与される権限情報です。なのでポリシーのないロールは何も権限がない状態なのでまずはポリシーを作成してロールを作成していきましょう。

### ポリシーの作成
#### 作成手順
1. IAMページに行って、サイドバーの「ポリシー」選択
2. 「ポリシーの作成」ボタン押下
3. JSONタブを開いて下記に記載したJSON内容をコピペして、「ポリシーの確認」押下
4. それぞれのポリシー名を入力する

下記の4つのポリシーを作成する。

1. AmazonSSMReadAccess
2. AmazonECSTaskExecutionRolePolicy
3. AmazonEC2ContainerServiceforEC2Role
4. AmazonECSServiceRolePolicy


##### AmazonSSMReadAccess

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameters",
                "secretsmanager:GetSecretValue",
                "kms:Decrypt"
            ],
            "Resource": "*"
        }
    ]
}
```

##### AmazonECSTaskExecutionRolePolicy

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
```

##### AmazonEC2ContainerServiceforEC2Role

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeTags",
                "ecs:CreateCluster",
                "ecs:DeregisterContainerInstance",
                "ecs:DiscoverPollEndpoint",
                "ecs:Poll",
                "ecs:RegisterContainerInstance",
                "ecs:StartTelemetrySession",
                "ecs:UpdateContainerInstancesState",
                "ecs:Submit*",
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
```

##### AmazonECSServiceRolePolicy

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ECSTaskManagement",
            "Effect": "Allow",
            "Action": [
                "ec2:AttachNetworkInterface",
                "ec2:CreateNetworkInterface",
                "ec2:CreateNetworkInterfacePermission",
                "ec2:DeleteNetworkInterface",
                "ec2:DeleteNetworkInterfacePermission",
                "ec2:Describe*",
                "ec2:DetachNetworkInterface",
                "elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
                "elasticloadbalancing:DeregisterTargets",
                "elasticloadbalancing:Describe*",
                "elasticloadbalancing:RegisterInstancesWithLoadBalancer",
                "elasticloadbalancing:RegisterTargets",
                "route53:ChangeResourceRecordSets",
                "route53:CreateHealthCheck",
                "route53:DeleteHealthCheck",
                "route53:Get*",
                "route53:List*",
                "route53:UpdateHealthCheck",
                "servicediscovery:DeregisterInstance",
                "servicediscovery:Get*",
                "servicediscovery:List*",
                "servicediscovery:RegisterInstance",
                "servicediscovery:UpdateInstanceCustomHealthStatus"
            ],
            "Resource": "*"
        },
        {
            "Sid": "ECSTagging",
            "Effect": "Allow",
            "Action": [
                "ec2:CreateTags"
            ],
            "Resource": "arn:aws:ec2:*:*:network-interface/*"
        },
        {
            "Sid": "CWLogGroupManagement",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:DescribeLogGroups",
                "logs:PutRetentionPolicy"
            ],
            "Resource": "arn:aws:logs:*:*:log-group:/aws/ecs/*"
        },
        {
            "Sid": "CWLogStreamManagement",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:DescribeLogStreams",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:log-group:/aws/ecs/*:log-stream:*"
        }
    ]
}
```



### ロールの作成
IAMページに行って、サイドバーの「ロール」→「ロールの作成」より下記のロールを作成する。
作成後、各ロールのページにて「ポリシーをアタッチする」を押下して上記で作成したポリシーを紐づける。

1. ecsInstanceRole（→AmazonEC2ContainerServiceforEC2Roleに紐づける）
2. AWSServiceRoleForECS（→AmazonECSServiceRolePolicyに紐づける）
3. ecsTaskExecutionRole（→AmazonECSTaskExecutionRolePolicy,AmazonSSMReadAccessを紐づける）

参考
[Amazon ECS タスク実行 IAM ロール](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_execution_IAM_role.html)


## ALBの作成
ECSのサービス作成時にALBを登録しておけば、コンテナに動的にポートマッピングをしてくれるようになるので楽になります。

1. Application Load Balancerを選択
2. 名前を入力。サブネットを二つ選択。（ない場合は、適宜作成）
3. セキュリティグループを選択。（ない場合は、適宜作成）
4. ターゲットグループを選択or作成
5. ターゲットグループにインスタンスを登録

## クラスターの作成
ECSのサイドバーにある「クラスター」から「クラスターの作成」ボタンを押下
「クラスターテンプレートの選択」は「EC2 Linux + ネットワーキング」を選択
1. クラスター名記載
2. EC2インスタンスタイプの選択（お好み）
3. キーペア（お好み。ただし、デバッグ時にSSHできた方がいいので設定しておくことをおすすめ）
4. コンテナインスタンスの IAM ロールに「ecsInstanceRole」を選択

## RDSの作成
aws-cliでのRDS作成。
コンソール上からでもOKです。

```
	aws rds create-db-instance \
			--db-instance-identifier rails-sample-db-production \
			--db-instance-class db.t2.micro \
			--db-subnet-group-name rails-sample-db-subnet-group \
			--engine mysql \
		  --engine-version 5.7.26 \
			--allocated-storage 20 \
			--master-username [username] \
			--master-user-password [password] \
			--backup-retention-period 3 \
```

参考
[AWS CLI を使って RDS を作成する (自分用メモ) - Qiita](https://qiita.com/propella/items/807ec930c9f6c9f6d5b8)
[AWS-CLI Amazon Aurora インスタンス作成 - Qiita](https://qiita.com/web_se/items/e5a2fd30bc4e798cf5d1)

## AWS Systems Managerの設定
AWS Systems Managerは、タスク実行時にコンテナに注入する秘匿情報（環境変数）の管理に使えるAWSサービスです。
初めての人は設定の仕方を含め、
[ECSでごっつ簡単に機密情報を環境変数に展開できるようになりました！](https://dev.classmethod.jp/cloud/aws/ecs-secrets/)
を見れば大体分かると思います。

[AWS Systems Manager](https://ap-northeast-1.console.aws.amazon.com/systems-manager/)の左側メニューから「パラメータストア」→「パラメータの作成」をクリック。パラメータの詳細画面が表示されるので、パラメータのキー名と値を入力します。タイプには「安全な文字列」を選択します。

パラメータのキー名と値一覧

| キー名 | 値 |
|:---------|:--------:|
| /production/database_username | [RDSに設定したusername] |
| /production/database_password | [RDSに設定したpassword] |
| /production/database_host | [RDSインスタンスのエンドポイント] |

RDSインスタンスのエンドポイント（RDS→データベース→[インスタンス名]）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213965/0db067ce-dbc5-cd94-ef6d-be35eed90fc3.png)


# CircleCIの設定
```.circleci/config.yml
version: 2.1
orbs:
  aws-cli: circleci/aws-cli@0.1.13
executors:
  builder:
    docker:
      - image: circleci/buildpack-deps

commands:
  init:
    steps:
      - checkout
      - aws-cli/install
      - install_ecs-cli
      - setup_remote_docker
  install_ecs-cli:
    steps:
      - run:
          name: Install ECS-CLI
          command: |
            sudo curl -o /usr/local/bin/ecs-cli https://amazon-ecs-cli.s3.amazonaws.com/ecs-cli-linux-amd64-latest
            sudo chmod +x /usr/local/bin/ecs-cli

jobs:
  build:
    executor: builder
    steps:
      - init
      - run:
          name: Build application Docker image
          command: |
            docker build -f build.Dockerfile --rm=false -t rails-sample-app-build:latest .
      - run:
          name: Save image
          command: |
            mkdir -p /tmp/docker
            docker save rails-sample-app-build:latest -o /tmp/docker/image
      - persist_to_workspace:
          root: /tmp/docker
          paths:
            - image
  deploy:
    executor: builder
    steps:
      - init
      - attach_workspace:
          at: /tmp/docker
      - run: docker load -i /tmp/docker/image
      - run:
          name: Assets precompile and Push Docker image
          command: |
            docker build -f assets.Dockerfile --build-arg RAILS_MASTER_KEY=${RAILS_MASTER_KEY} --rm=false -t rails-sample-app-build:latest .
      - run:
          name: Push Docker image
          command: |
            ecs-cli push rails-sample-app-build:latest
      - run:
          name: ECS Config
          command: |
            ecs-cli configure \
            --cluster rails-sample-${CIRCLE_BRANCH} \
            --region ${AWS_DEFAULT_REGION} \
            --config-name rails-sample-${CIRCLE_BRANCH}
      - run:
          name: migrate deploy
          command: |
            ecs-cli compose \
            --file ecs/${CIRCLE_BRANCH}/migrate/docker-compose.yml \
            --ecs-params ecs/${CIRCLE_BRANCH}/migrate/ecs-params.yml \
            --project-name rails-sample-${CIRCLE_BRANCH}-migrate \
            up \
            --launch-type EC2 \
            --create-log-groups \
            --cluster-config rails-sample-${CIRCLE_BRANCH}
      - run:
          name: Unicorn + Nginx deploy
          command: |
            ecs-cli compose \
            --file ecs/${CIRCLE_BRANCH}/app/docker-compose.yml \
            --ecs-params ecs/${CIRCLE_BRANCH}/app/ecs-params.yml \
            --project-name rails-sample-${CIRCLE_BRANCH}-app \
            service up \
            --container-name nginx \
            --container-port 80 \
            --target-group-arn ${TARGET_GROUP_ARN} \
            --timeout 0 \
            --launch-type EC2 \
            --create-log-groups \
            --cluster-config rails-sample-${CIRCLE_BRANCH}

workflows:
  version: 2
  build-deploy:
    jobs:
      - build
      - deploy:
          requires:
            - build
          filters:
            branches:
              only:
                - master

```

## CircleCIに設定する環境変数


CircleCIのプロジェクトの設定ページ（Settings→[アカウント名or組織名]→[プロジェクト名]）に行き、下記の画像の箇所から設定する
https://circleci.com/gh/[アカウント名or組織名]/[プロジェクト名]/edit#env-vars

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213965/664e7ef6-42a4-e97b-7eed-db9ccaff9986.png)


| 環境変数名 | 値 |
|:---------|:--------:|
| AWS_ACCESS_KEY_ID | [AWSのアクセスキーID] |
| AWS_ACCOUNT_ID | [AWSのアカウントID] |
| AWS_DEFAULT_REGION | [AWSのデフォルトリージョン] |
| AWS_ECR_REPOSITORY_URL | [AWSのECRリポジトリURL] |
| AWS_SECRET_ACCESS_KEY | [AWSのシークレットアクセスキー] |
| RAILS_MASTER_KEY | [config/master.keyの値] |
| TARGET_GROUP_ARN | [ターゲットグループのarn] |

# Task definitionの作成

## docker-compose.yml
```rails-sample/ecs/production/app/docker-compose.yml
version: "3"

services:
  app:
    image: [ECRのリポジトリURI]
    entrypoint: bundle exec unicorn -c config/unicorn.rb
    env_file:
      - ../env
    working_dir: /projects/rails-sample
    logging:
      driver: "awslogs"
      options:
        awslogs-region: "ap-northeast-1"
        awslogs-group: "rails-sample-production/app"
        awslogs-stream-prefix: "rails-sample-app"
  nginx:
    image: [ECRのリポジトリURI]
    ports:
      - 0:80
    links:
      - "app:app"
    env_file:
      - ../env
    working_dir: /projects/rails-sample
    logging:
      driver: "awslogs"
      options:
        awslogs-region: "ap-northeast-1"
        awslogs-group: "rails-sample-production/nginx"
        awslogs-stream-prefix: "rails-sample-nginx"

```

＊ Nginxの設定ファイルは適宜用意してください。上記のnginxの欄にnginx設定ファイル群の設置・起動用のスクリプト`entrypoint: /bin/bash /etc/nginx/start.sh`を用意するなど。


## ecs-params.yml
タスク実行時に実行ロールの指定やコンテナに注入する環境変数をAWS Systems Managerから取得するして設定するためのファイル

```rails-sample/ecs/production/app/ecs-params.yml
version: 1
task_definition:
  # タスク実行時のロールを指定
  task_execution_role: ecsTaskExecutionRole
  services:
    # 起動するコンテナを記載(app, nginx)
    app:
      # 何らかの理由で失敗・停止した際に、タスクに含まれる他のすべてのコンテナを停止するかどうか(デフォルトはtrue)
      essential: true
      # AWS Systems Managerから秘匿情報を取得してコンテナに環境変数を注入
      secrets:
        - value_from: /production/database_username
          name: DATABASE_USERNAME
        - value_from: /production/database_password
          name: DATABASE_PASSWORD
        - value_from: /production/database_host
          name: DATABASE_HOST
    nginx:
      essential: true
run_params:
  network_configuration:
    awsvpc_configuration:
      assign_public_ip: ENABLED

```


## コンテナ全体に注入する環境変数の設定
各環境(production, stagingなど)ごとのディレクトリ以下に`env`ファイルを用意してそこに記載する

```env
# ここのファイルに追加した環境変数は全てのコンテナに展開されます
# Rails
APP_HOST=54.238.241.230
RAILS_ENV=production
RAILS_LOG_TO_STDOUT=1
RAILS_SERVE_STATIC_FILES=1

# RDS
DATABASE_NAME=rails-sample_production
DATABASE_PORT=3306
DATABASE_POOL=10

# Unicorn
UNICORN_PORT=23380
UNICORN_TIMEOUT=180
UNICORN_WORKER_PROSESSES=2

# Nginx専用
NGINX_APP_SERVER_NAME=app
NGINX_APP_SERVER_PORT=23380
NGINX_DOCUMENT_ROOT=/projects/rails-sample/public
NGINX_FRONT_SERVER_NAME=54.238.241.230
```




# 構築の際に詰まる可能性のあるポイント
## ECSコンテナインスタンスの作成

- インスタンスへのIAMロールを付与すること
- ecs-agentのインストール ( [Amazon ECS コンテナエージェントのインストール - Amazon Elastic Container Service](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-agent-install.html) )
- EC2インスタンスの`/etc/ecs/ecs.config`に`CLUSTER_NAME=クラスター名`の登録
- 所属するクラスターを変更する場合、 `/var/lib/ecs/data/ecs_agent_data.json` を削除してからecs-agentを再起動する

Defaultクラスター作成しているし、IAMロールにecs:CreateClusterの権限付与されているから自動で作成なんかもしてくれるのかと思ったら作成してくれなかった。
なので、クラスター作成→インスタンス作成の方が良い（ちな、クラスター作成時にインスタンスも作成するようにはできるっぽい）
→カスタマイズされてるAMI利用時のみ初期スクリプトによってDefaultクラスターを作成しているのかもしれない

:hatched_chick: 参考 :hatched_chick: 
[Amazon ECS コンテナインスタンスの起動 - Amazon Elastic Container Service](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/launch_container_instance.html)
[Amazon ECS-optimized AMI - Amazon Elastic Container Service](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-optimized_AMI.html)

### インスタンスタイプについて

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213965/f2186f98-4011-0a09-2318-c33fbfcb1efe.png)



ある程度余裕持たないとタスク実行するための容量を持たなくて死ぬ
（ほんとは、ローカルや本番環境で動かした時の使用量見てタスク実行に必要なメモリを設定した方が良い）

## ecs-cliでのタスク実行
* `ecs-params.yml` ファイル内で`task_execution_role`を指定すること
* `task_execution_role`で指定した適切なポリシーを適用したIAM Roleを用意すること（エラーが出なくて、単純に実行されないので気づきにくい）

# まとめ
ECSについてググればたくさん記事出てくるのですが、実際に活用しようとしてみるとたくさん落とし穴があります。もし利用しようか考えている人は一度デモアプリで利用してみることをお勧めします。

# 最後に
UUUMではインフラに詳しいエンジニアを欲しています。
詳しくはこちら →→→→→→ [UUUM攻殻機動隊の紹介](https://system.blog.uuum.jp/entry/2018/12/01/132950)