AWS Copilotを使ってRailsコンテナの本番環境を爆速で構築する

# やりたいこと
Amazon ECSで動くRails本番環境を手軽に作りたい。

# やりたくないこと
- ECSを動かすためのネットワーク構築やロードバランサーの設定
- AWSコンソールを利用したコンテナのデプロイ
- 複雑なCI/CDフローの構築
  - [AWS Copilotを使ってRailsアプリケーションのCI/CDパイプラインを爆速で構築する](https://qiita.com/sshon/items/954bd16c798a104293c0)

### →こういうときにはAWS Copilot!

# AWS Copilot
AWSでコンテナ化されたアプリケーションの開発、リリースを容易に行うためのコマンドラインツールです。
コマンドを叩くとCloudFormationが動き、必要なリソースの作成やデプロイを行うことができる。CI/CDパイプラインもコマンド一つで作成できます。

※Fargate起動タイプのみサポートしています

### Copilotを支える概念
<img width="784" alt="スクリーンショット 2020-08-06 1.23.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/86fd6efe-4299-fb21-ff50-40ddc87de5eb.png">
https://github.com/aws/copilot-cli/wiki/Environments

#### Service
ECS上で動くコンテナアプリケーションのことです。
主にECSのサービスやタスク定義と関連があります。

#### Environment
本番環境やステージング環境といったステージのことです。
Environmentが異なるとVPCレベルで異なります。
主に以下のリソースと関連があります。
- VPC, Subnet
- ECS Cluster
- ALB, Security Group, Internet Gateway
- Route53

#### Application
ServiceとEnvironmentを束ねたひとまとまりのことです。

# Copilotを使ったRailsコンテナ環境の作り方
今回はNginxをかませず、直接ALBからRailsのコンテナにアクセスする仕組みを作ります。

### 手順概要
1. ローカルで動くRailsコンテナアプリケーションを用意
2. CopilotのインストールとAWS credentialsの設定
3. Applicationの作成
4. Environmentの作成
5. RDSインスタンスの作成
6. Serviceの作成

以下のコマンドでApplication、Environment、Serviceの作成を全て行ってくれますが、Serviceの作成の前にRails起動のためのデータベース(ここではRDSを利用)が必要なので、以下コマンドは使わずに一つ一つ手順を行っていきます。

```
$ copilot init
```

#### 1. ローカルで動くRailsコンテナアプリケーションを用意
ローカルで`docker-compose up`コマンドで起動するRailsアプリケーションを作成します。
[Rails on DockerのQuickstartをalpine linuxでやってみる](https://qiita.com/at-946/items/c69a512ea47941747b18)を参考にしつつ、APIモードで作成しました。

なお、プロジェクトフォルダ名にはアンダースコアは利用しないほうが良いです。後々、copilotを利用してCI/CDパイプラインを作成する際にCloudFormationの命名規則に引っかかりエラーになります。

利用したDockerfileとdocker-compose.ymlは以下の通りです。

```.dockerfile:Dockerfile
FROM ruby:2.7.1-alpine3.12

ENV ROOT="/myapp" \
    LANG=C.UTF-8 \
    TZ=Asia/Tokyo

WORKDIR ${ROOT}

RUN apk update && \
    apk upgrade && \
    apk add --no-cache \
        tzdata \
        nodejs \
        mysql-dev \
        mysql-client \
        vim && \
    apk add --virtual build-packs --no-cache \
        build-base \
        curl-dev \
        gcc \
        g++ \
        libc-dev \
        libxml2-dev \
        linux-headers \
        make

COPY Gemfile ${ROOT}
COPY Gemfile.lock ${ROOT}

RUN bundle install
RUN apk del build-packs

COPY . ${ROOT}

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]
```

```docker-compose.yml
version: '3'

services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_DATABASE: myapp
      MYSQL_USER: root
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      TZ: Asia/Tokyo
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
    ports:
      - "3306:3306"
    volumes:
      - db_volume:/var/lib/mysql

  web:
    build: .
    command: ash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db

volumes:
  db_volume:
```

取り急ぎ、コンテナ起動時にデータベースの作成とマイグレーションを行うために、entrypoint.shにコマンドを記述します。
マイグレーション用のECSタスクを作成して、適宜実行するのが良いかもしれません。

```entrypoint.sh 
#!/bin/sh
set -e

rm -f /myapp/tmp/pids/server.pid
bundle exec rails db:create
bundle exec rails db:migrate

exec "$@"
```

さらに`.dockerignore`でmaster.keyをビルド対象から除外している場合は、ビルド対象として含めるようにコメントアウトします。
本来は、AWS Systems Managerのパラメータストアにmaster.keyの文字列を登録して、copilotから参照できるように`manifest.yml`(後述)のsecretsを登録すべきです。

```.dockerfile:.dockerignore
# Ignore master key for decrypting credentials and more.
# /config/master.key
```

またこの段階で、ALBのヘルスチェックに引っかかってコンテナの停止→起動を繰り返さないためにも、ヘルスチェック応答用のgemである[okcomputer](https://github.com/sportngin/okcomputer)を導入しました。
ルートアクセスに対して応答させるために以下のような記述をします。

```config/initializers/okcomputer.rb
OkComputer.mount_at = '/'
```

ルートアクセスして以下のような表示が返れば準備完了です。

|<img width="554" alt="スクリーンショット 2020-08-05 12.56.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/69c37248-d3b6-3c6a-d47e-60880bddf50f.png">
|:-:|


#### 2. CopilotのインストールとAWS credentialsの設定

Copilotのインストール

```
$ brew install aws/tap/copilot-cli
```

AWS credentialsの設定(設定の仕方は[設定の基本](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-quickstart.html)を参照)
リソースを作成したいAWSアカウント情報をdefaultのprofileに設定しておきます。

```yml:~/.aws/credentials
[default]
aws_access_key_id=XXXXXXXXXXXXXXXXXXX
aws_secret_access_key=YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
```

#### 3. Applicationの作成
copilotコマンドの多くは対話型で操作が進みます。

Applicationが既に存在する場合は、既存のApplicationを利用するか確認されます。
今回は「copilot-demo」という名前のApplicationを新規に作成します。

```
$ copilot app init


  Would you like to use one of your existing applications? (Y/n)
  > n

  What would you like to name your application? [? for help] 
  > copilot-demo
```

しばらく経過すると以下のログが表示され、CloudFormationでRoleが作成されたことがわかります。
ローカルではApplication名が書かれた「copilot/.workspace」というファイルが作成されます。

```
  Use existing application: No
  Application name: copilot-demo
  ✔ Created the infrastructure to manage services under application copilot-demo.

  ✔ The directory copilot will hold service manifests for application copilot-demo.

  Recommended follow-up actions:
  - Run `copilot init` to add a new service to your application.
```

<img width="702" alt="スクリーンショット 2020-08-06 3.10.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/1b5074ac-a8fc-d5ac-62ad-47dceb8d9d73.png">

#### 4. Environmentの作成
次にEnvironmentを作成します。
今回は「production」という名前で作成しました。

```
$ copilot env init


  What is your environment's name? [? for help]
  > production

  Which named profile should we use to create production?  [Use arrows to move, type to filter, ? for more help]
  > default
```

しばらく経過すると以下のログが表示され、ネットワーク周りのリソース、ECSクラスタ、ALBが作成されたことがわかります。
もしApplication作成時にRoute53に指定しているドメインを指定すれば、Route53のレコードも作成されます。

CloudFormationで「EnvironmentSecurityGroup」というセキュリティグループが作成されます、後ほどRDSセキュリティグループのインバウンドグループのソースとするためにIDを保存しておきます。

```
  What is your environment's name? production
  Which named profile should we use to create production? default
  ✔ Created the infrastructure for the production environment.
  - Virtual private cloud on 2 availability zones to hold your services     [Complete]
  - Virtual private cloud on 2 availability zones to hold your services     [Complete]
    - Internet gateway to connect the network to the internet               [Complete]
    - Public subnets for internet facing services                           [Complete]
    - Private subnets for services that can't be reached from the internet  [Complete]
    - Routing tables for services to talk with each other                   [Complete]
  - ECS Cluster to hold your services                                       [Complete]
  - Application load balancer to distribute traffic                         [Complete]
  ✔ Linked account XXXXXXXXXXX and region ap-northeast-1 to application copilot-demo.

  ✔ Created environment production in region ap-northeast-1 under application copilot-demo.
```

#### 5. RDSインスタンスの作成
Service作成時にデータベースに接続できないというエラーを回避するために、このタイミングでRDSインスタンスを作成します。
Aurora Serverlessを利用していますが、何を利用しても問題ありません。

<img width="583" alt="スクリーンショット 2020-08-05 11.37.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/ff036f05-9e95-a3ca-98ea-1af6766b7f09.png">
<img width="583" alt="スクリーンショット 2020-08-05 11.39.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/5cd84c58-b583-062d-7dfa-b1a0460fdc59.png">
<img width="583" alt="スクリーンショット 2020-08-05 11.39.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/a510c190-6307-baee-f193-4106d592517d.png">

作成後は、Environment作成時に作成された「EnvironmentSecurityGroup」を、RDSセキュリティーグループのインバウンドルールのソースに設定します。
<img width="934" alt="スクリーンショット 2020-08-05 13.01.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/b4415e2a-19b1-6fc5-c52f-983f830e2207.png">

また、作成したRDSインスタンスの情報をdatabase.ymlに反映させます。

```config/database.yml
production:
  <<: *default
  database: myapp_production
  host: copilot-demo-database.cluster-csfienv6hggj.ap-northeast-1.rds.amazonaws.com
  username: admin
  password: 5ohWzjXPr7w7u8OZgyBw
```

#### 6. Serviceの作成
最後にServiceを作成します。

Serviceの種類として以下の2種類があります。今回は「Load Balanced Web Service」を選択し、名前は「api」としました。

- Load Balanced Web Service: インターネットから接続がある場合に選択
- Backend Service: インターネットから接続がない場合に選択

※上記の選択によって作成されるリソースに差が出る(インターネットゲートウェイ等)

```
$ copilot svc init


  Which service type best represents your service's architecture?  [Use arrows to move, type to filter, ? for more help]
  > Load Balanced Web Service
    Backend Service

  What do you want to name this Load Balanced Web Service? [? for help]
  > api

  Which Dockerfile would you like to use for api?  [Use arrows to move, type to filter, ? for more help]
  > ./Dockerfile
```

しばらく経過すると以下のログが表示され、CloudFormationでECSサービスやECSタスク定義が作成されます。
ローカルでは「copilot/api/manifest.yml」というファイルが作成されます。

```
  Service type: Load Balanced Web Service
  Service name: api
  Dockerfile: ./Dockerfile
  ✔ Wrote the manifest for service api at copilot/api/manifest.yml
  Your manifest contains configurations like your container size and port (:3000).

  ✔ Created ECR repositories for service api.

  Recommended follow-up actions:
  - Update your manifest copilot/api/manifest.yml to change the defaults.
  - Run `copilot svc deploy --name api --env test` to deploy your service to a test environment.
```

##### manifest.yml
Serviceの定義をするファイルです。
Dockerイメージ、コンテナの接続ポート、アクセスを許すURLパス、ECSタスクのCPU/メモリ、ECSサービス内のタスク数、環境変数、シークレット(参照できるAWS Systems Managerのパラメータ)を指定できます。

本番環境としてRailsコンテナを起動するためにRAILS_ENVを環境変数で渡します。
通常は「RAILS_ENV = development」ですが、CopilotのEnvironmentがproductionの時は「RAILS_ENV = production」という記述です。

```manifest.yml
name: api
type: Load Balanced Web Service

image:
  build: ./Dockerfile
  port: 3000

http:
  path: '/'

cpu: 256
memory: 512
count: 1

variables:
  RAILS_ENV: development

environments:
  production:
    variables:
      RAILS_ENV: production
```

最後に、以下コマンドを叩き、定義したServiceをデプロイします。
以下が実行されます。
- ローカルでDockerイメージのビルド
- DockerイメージをECRへのプッシュ
- ECSタスク更新
- ECSサービス更新

```
$ copilot deploy
```

```
  ...
  Successfully tagged XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/copilot-demo/api:77673f1
  Login Succeeded
  The push refers to repository [XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/copilot-demo/api]
  cee202204539: Pushed 
  2bf6e538dc23: Pushed 
  788fae7f9c70: Pushed 
  0ee2627f68ac: Pushed 
  246acc754b43: Pushed 
  68a27f30bbbc: Pushed 
  bb97d43854fb: Pushed 
  9099c9ee41ff: Pushed 
  3d0400229c5c: Pushed 
  93a2bfafa84f: Pushed 
  54f362ba164c: Pushed 
  c4b1ff92c516: Pushed 
  446d8e2016ac: Pushed 
  50644c29ef5a: Pushed 
  77673f1: digest: sha256:40d94e7257d0657eb2a69450caa3b9f81f33c258719f57a0cafcd857aae2e123 size: 3245


  ✔ Deployed api, you can access it at http://copil-Publi-1R5AI7MW2C3Q3-974847059.ap-northeast-1.elb.amazonaws.com.
```

実行ログ行末に記載のあるURLにアクセスすると正常に表示されており、ログを見ると本番環境として動作していることが確認できました。

画面

|<img width="328" alt="スクリーンショット 2020-08-06 4.11.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/4ec5bbdf-dd46-c0e0-cabb-c3d940acb079.png">
|:-:|

CloudWatchログ

```
  Database 'myapp_production' already exists
  => Booting Puma
  => Rails 6.0.3.2 application starting in production
  => Run `rails server --help` for more startup options
  Puma starting in single mode...
  * Version 4.3.5 (ruby 2.7.1-p83), codename: Mysterious Traveller
  * Min threads: 5, max threads: 5
  * Environment: production
  * Listening on tcp://0.0.0.0:3000
  Use Ctrl-C to stop
```

# まとめ
今回作成したコードは以下です。
https://github.com/ssshun/rails-copilot

AWS Copilotを利用して、AWSコンソールを利用した操作を極力減らし、手軽にRailsコンテナを本番稼働させることができました。
CI/CDパイプラインの作成はハマりどころが多かったので別途記載します。