ECSを高速に構築できるAWS Copilot CLIを試してみた

こんにちは、[DMM WEBCAMP AdventCalender](https://qiita.com/advent-calendar/2020/infratop) の20日目を担当します、AWSとDockerが好きな[@tkt_horikoshi](https://twitter.com/tkt_horikoshi)です。

今回はコマンドラインベースで[ECS](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/Welcome.html)を構築できる[AWS Copilot](https://aws.github.io/copilot-cli/)を試してみたので使い心地を共有していきたいと思います。

# はじめに

DMM WEBCAMPでエンジニアメンターを始めてからまだ日は浅いですが、受講生さんの中にDockerで開発環境を整えている方がいらっしゃるみたいでした。

せっかくDockerで開発してるならEC2に直接ファイルを置いたりせずにコンテナベースでアプリケーションを実行できるようにしたい。でも、何から始めたらいいかわからん。

そんな思いを抱いている人が一歩踏み出すきっかけになれば幸いです。

# ECSとは

[ECS](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/Welcome.html)はAmazon Elastic Container Serviceの略称で、Dockernizeされたアプリケーションをデプロイし、実行環境を提供するサービスです。コンピューティングリソースにEC2とFargateを選択できますが、特に[Fargate](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/AWS_Fargate.html)はコンテナ向けコンピューティングエンジンでプロビジョニングおよびメンテナンスコストが不要になるといった特徴があります。

とても魅力的なサービスで、私も本業でお世話になっているのですが、イチから構築するとなるとそこそこコストがかかるというのも事実です。

# AWS Copilotを触ってみる

AWS CopilotはコマンドラインベースでコンテナアプリケーションをサクッとECSへデプロイできるというものです。コマンドラインからECS作れるなんてにわかに信じがたいですがやっていこうと思います。

### 基本用語

Copilotを利用するうえで理解しておきたい[コンセプト](https://aws.github.io/copilot-cli/docs/concepts/overview/)についてについてまとめました。Copilotを使ってコマンドラインから下記をそれぞれ作成することができます。

| コンセプト | 説明 |
|:----|:----|
| Application | 例えばチャットアプリや、ブログサイトといった開発プロダクトそのものを示す概念です。ApplicationにはService, Environmentといった概念が含まれています。 |
| Service | ServiceはFrontend, Backendといったアプリケーションの構成要素を示します。 |
| Environment | Environmentはテスト環境, プロダクション環境というようなアプリケーションの動作環境の役割を示します。 |
|Job|イベント駆動で一時的に実行される処理（ECSタスク）を示します。|
|Pipeline|ビルド、テスト、デプロイといった一連の作業を行うリリースフローを示します。|

今回の記事では主にApplication, Service, Environmentについて触れていきます。

### 本日のゴール

Copilotを使ってアプリケーションをECSにデプロイし、Railsのウェルカム画面を表示してみたいと思います。

# アプリケーションの準備

### Railsの準備

#### Dockerfile

まずは、アプリケーションをDockerで動くように設定します。ルートディレクトリにDockerfileを配置します。

```dockerfile
FROM ruby:2.7

RUN mkdir /app
WORKDIR /app

RUN curl https://deb.nodesource.com/setup_12.x | bash
RUN curl https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

RUN apt-get update -qq && \
    apt-get install -y nodejs yarn

COPY Gemfile* ./
RUN bundle install
COPY . .

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"]
```

#### config.hostsの指定

ドメイン許可のため`config/environments/development.rb`, `config/environments/production.rb`に`config.hosts`を追加してドメインを指定しておきます。

今回は、テスト環境とプロダクション環境それぞれのサブドメインも許可したいので下記のような指定となります。

```ruby
Rails.application.configure do
...

  config.hosts << ".takat0-h0rikosh1.com"

...
end
```

### Nginxの準備

Nginxも同様にDockerで動くように設定します。ルートディレクトリに`nginx`というディレクトリをきって各種設定ファイルを配置します。

`${SERVER_HOST}` を環境変数で切り替えられるようにする都合で下記通り設定ファイルのテンプレートを用意しました。

```nginx:nginx/nginx.conf.template
upstream app {
  server ${SERVER_HOST}:3000;
}

server {
  listen 80;
  server_name localhost;

  root /app/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 @app;
  keepalive_timeout 5;

  location / {
    proxy_pass http://app;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_redirect off;
    proxy_connect_timeout 30;
  }
}
```

続いてDockerfileです。CMDで`envsubst`を使いで変数を差し替えて`/etc/nginx/conf.d/nginx.conf`に出力します。`SERVER_HOST`のデフォルトは`localhost`としてます。

```dockerfile:nginx/Dockerfile
FROM nginx:alpine

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

ADD nginx.conf.template /etc/nginx/conf.d/nginx.conf.template

ENV SERVER_HOST localhost

CMD envsubst \
    '$$SERVER_HOST' \
    < /etc/nginx/conf.d/nginx.conf.template > \
    /etc/nginx/conf.d/nginx.conf \
    && nginx -g 'daemon off;'
```

### 動作確認

docker-compose.ymlを作成して動作確認してみます。

```yaml:docker-compose.yml
version: "3.8"

services:
  app:
    build: .
    volumes:
      - .:/app
    ports:
      - "3000:3000"
    environment:
      RAILS_ENV: development

  proxy:
    build:
      context: nginx
    ports:
      - 80:80
    depends_on:
      - app
    environment:
      SERVER_HOST: host.docker.internal
```

コンテナを起動立ち上げてみましょう

```sh
$ docker-compose up
Recreating copilot-playground_app_1 ... done
Recreating copilot-playground_proxy_1 ... done
Attaching to copilot-playground_app_1, copilot-playground_proxy_1
app_1    | => Booting Puma
app_1    | => Rails 6.0.3.4 application starting in development 
app_1    | => Run `rails server --help` for more startup options
app_1    | Puma starting in single mode...
app_1    | * Version 4.3.7 (ruby 2.7.2-p137), codename: Mysterious Traveller
app_1    | * Min threads: 5, max threads: 5
app_1    | * Environment: development
app_1    | * Listening on tcp://0.0.0.0:3000
app_1    | Use Ctrl-C to stop
```

ひとまず手元での動作確認はできました。

![スクリーンショット 2020-12-20 12.25.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142263/57670c0a-31b0-76ce-d1e5-f9524ad07c25.png)

# Copilotを使ってデプロイしてみる

### Install

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

参考: https://github.com/aws/copilot-cli

### Applicationを作成する

自身の所有するドメインを指定して**Application**を作成します。**Application**の名前は`welcome`にします。ルートディレクトリで [copilot app init](https://aws.github.io/copilot-cli/docs/commands/app-init/) を実行します。

```sh
$ copilot app init welcome --domain takat0-h0rikosh1.com


✔ Created the infrastructure to manage services and jobs under application welcome.

✔ The directory copilot will hold service manifests for application welcome.

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

なにやら [copilot init](https://aws.github.io/copilot-cli/docs/commands/init/) を実行することを進められます。

### Serviceの作成とテスト環境のプロビジョン・デプロイ

実行ログに促されるまま [copilot init](https://aws.github.io/copilot-cli/docs/commands/init/) を実行するとステップバイステップで**Service**の設定を進めていくことができます。

```sh
$ copilot init

Welcome to the Copilot CLI! We're going to walk you through some questions
to help you get set up with an application on ECS. An application is a collection of
containerized services that operate together.
```

はじめに**Service**のワークロードタイプについて聞かれるのでここは`Load Balanced Web Service`を選択します。名前をどうするか聞かれるので**app**としました。

```sh
  Which workload type best represents your architecture?  [Use arrows to move, type to filter, ? for more help]
  > Load Balanced Web Service
    Backend Service
    Scheduled Job

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

続いて、Dockerfileの選択を迫られます。ここではRailsのDockerfileを選択します。

```sh
for welcome?  [Use arrows to move, type to filter, ? for more help]
  > ./Dockerfile
    nginx/Dockerfile
    Enter custom path for your Dockerfile
    Use an existing image instead
```

最後に、テスト環境へデプロイするからどうか尋ねられるので `y` と入力します。

```sh
Would you like to deploy a test environment? [? for help] (y/N)
y
```

ここまでCLIとの対話が完了するとすごい勢いでプロビジョニングが始まり、数分後にデプロイが完了します。実行ログの最後にURLが表示されるのでブラウザで表示してみます。

![スクリーンショット 2020-12-20 23.43.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142263/e2894b7a-355f-3e48-0590-c80d38e16cae.png)

おー、出ました。

念の為、AWSマネジメントコンソールに入ってざっと内容を確認してみることにします。

- ECSクラスター
  - ![スクリーンショット 2020-12-20 23.49.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142263/7ec1e9f6-6aea-bfb7-42ad-7c7ebb623fbb.png)
- ECSサービス
![スクリーンショット 2020-12-20 23.50.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142263/aa3ca20d-5f35-e477-49e4-664da5a2ffdd.png)
- ALB
  - ![スクリーンショット 2020-12-21 0.03.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142263/e3da5b6a-d522-7274-2170-821fa0ebcb8c.png)


ネットワークの構成はどうでしょうか？

- VPC
  - ![スクリーンショット 2020-12-20 23.48.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142263/68885aa1-254a-fff8-1151-76670f36645b.png)
- サブネット
  - ![スクリーンショット 2020-12-20 23.48.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142263/9db327e2-3bcd-fa2f-f341-d6f88e9741b5.png)
- セキュリティーブループ
  - ![スクリーンショット 2020-12-20 23.56.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142263/471fadb0-7b8a-a588-a528-9f24d53d130a.png)

**Environment**の粒度でVPCも丸ごと用意してくれちゃってるみたいです。

すごいですね、コマンドラインだけでDNSの設定を含めECSでアプリケーションを動かすところまでたどり着きました。

# SidecarパターンでNginxを動かす

既にお気づきの方もいらっしゃるかと思いますが、この時点ではブラウザからのアクセスがALBを通じてRailsのサーバーに向かってしまいます。

そこで、Railsサーバーのコンテナが動いているECSタスクにNginxのコンテナを動かしてALBのトラフィックをプロキシできるようにします。

ECSタスク内であるコンテナに対して補佐的な役割のコンテナを動かすアプローチを[Sidecarパターン](https://aws.amazon.com/jp/blogs/compute/nginx-reverse-proxy-sidecar-container-on-amazon-ecs/)と呼びます。

### ECRにプッシュする

NginxのDockerイメージをECRにプッシュします。
https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/getting-started-cli.html

```sh
# リポジトリ作成
$ aws ecr create-repository --repository-name welcome/proxy

# イメージプッシュ
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 593653836732.dkr.ecr.ap-northeast-1.amazonaws.com && \
  docker build -t welcome/proxy nginx && \
  docker tag welcome/proxy:latest 593653836732.dkr.ecr.ap-northeast-1.amazonaws.com/welcome/proxy:latest && \
  docker push 593653836732.dkr.ecr.ap-northeast-1.amazonaws.com/welcome/proxy:latest
```

### マニフェストファイル修正

[copilot init](https://aws.github.io/copilot-cli/docs/commands/init/) を実行した時点で設定情報が記載されたマニフェストファイルが作成されているのでこれを下記のように修正します。

```yaml:copilot/app/manifest.yml
name: app
type: Load Balanced Web Service

image:
  build: ./Dockerfile
  port: 3000

http:
  path: 'app'
  # 追加
  healthcheck: '/'
  targetContainer: 'proxy'

cpu: 256
memory: 512
count: 1

# 追加
sidecars:
  proxy:
    port: 80
    image: 593653836732.dkr.ecr.ap-northeast-1.amazonaws.com/welcom/proxy:latest
```

### デプロイ

**Service**をデプロイする [copilot svc deploy](https://aws.github.io/copilot-cli/docs/commands/svc-deploy/) コマンドを実行します。

```sh
$ copilot svc deploy
```

以下の通り、ECSタスクに `app` と `proxy` のコンテナが実行されてることが確認できました。

![スクリーンショット 2020-12-21 1.38.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142263/d5cec0b8-fee3-67dd-b993-798be78f5d80.png)

追加した `proxy` コンテナへロードバランシングできていることが確認できました。

![スクリーンショット 2020-12-21 1.38.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142263/063b57de-1f02-fc00-7231-ab21d6ae8654.png)

ここまでで、Nginxでプロキシできるようになりました。

# Production環境のデプロイ

それでは、プロダクション環境のデプロイを試したいと思います。

### 環境変数の設定

今回はウェルカム画面を出したいだけなので実際には省きましたが、Production環境デプロイする場合は`RAILS_ENV`に`production`を設定する必要があると思うのですが、その際はマニフェストファイルに下記を追記します。


```yaml:copilot/app/manifest.yml
# 省略

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

参考: https://aws.github.io/copilot-cli/docs/developing/environment-variables/

### Environment作成

**Environment**を作成する [copilot env init](https://aws.github.io/copilot-cli/docs/commands/env-init/) のコマンドを`--prod`付きで実行します。名前は`production`としました。

```sh
$ copilot env init \
  --name production \
  --default-config \
  --prod
```

ここまでで、`VPC`と`ECS Cluster`が作成されていることが確認できます。

- VPC
  - ![スクリーンショット 2020-12-21 2.16.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142263/1e534174-b860-eb08-6fd4-aa8c0523a5f6.png)
- ECS Cluster
  ![スクリーンショット 2020-12-21 2.16.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142263/caec3139-c3bb-0e52-3e7a-957816d52a80.png)

ご覧の通り、サービスの実行まだ行われてません。


### Serviceデプロイ

[copilot svc deploy](https://aws.github.io/copilot-cli/docs/commands/svc-deploy/) を本番環境へ向けて実行してみることにします。

```sh
$ copilot svc deploy --env production

# 省略

✔ Deployed app, you can access it at https://app.production.welcome.takat0-h0rikosh1.com.
```

おー、出ました。

![スクリーンショット 2020-12-21 2.42.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142263/097c49a7-f720-4277-34ef-7fc6b2a2ed99.png)

コマンド２発でProductioin環境のデプロイが完了しました。

# Clean up

下記のコマンドでCopilotで作成した**Application**に含まれる全てのリソースがキレイに消えてくれます。不要であれば、無駄にお金を請求されないうちに消しておきましょう。

```sh
$ copilot app delete
```


# 最後に

AWS Copilotを使ってコマンドラインからECSを構築してみました。思っていた以上にサクサクできてしまいすごい時代になったものですね。特にネットワークの構成を意識させずにアプリケーションをホスティングするところまで持っていけるところがすごいです。使いこなせたら生産性がかなり上がりそうです。

時間を掛けずに試せるし、作るも消すもコマンドラインで自由自在ですので、アプリケーションを Dockernize したいけど腰が重いなあー、と感じていた人にとてもオススメです。

興味のある方はチャレンジしてみてください。

今回のソースコードはこちらに置いておきます。
https://github.com/takat0-h0rikosh1/dmm-webcamp-advent-calender-2020

# 参考

- https://aws.github.io/copilot-cli/
- https://github.com/aws/copilot-cli
- https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/Welcome.html
- https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/what-is-ecr.html
- https://qiita.com/minamijoyo/items/63ae57b99d4a4c5d7987
- https://qiita.com/kodai_0122/items/67c6d390f18698950440
- https://qiita.com/eighty8/items/0288ab9c127ddb683315