AWS Copilotで検証環境を構築した話

# はじめに
こんにちは、GxPの@ttanaka-gxpです。
この記事はグロースエクスパートナーズ Advent Calendar 2021の16日目です。

今回は以前から興味のあったAWS Copilotについて、ちょうど業務の中でECS上で動作させるAPIを構築する作業があり、そちらで構築しながら試させてもらった内容を、サンプルアプリも交えつつまとめようと思います。

# AWS Copilot CLIについて
[公式](https://aws.github.io/copilot-cli/ja/docs/overview/)から抜粋
> Copilot CLI は、AWS App Runner、Amazon ECS および AWS Fargate を利用したプロダクションレディなコンテナ化されたアプリケーションのビルド、リリース、そして運用のためのツールです。 開発のスタートからステージング環境へのプッシュ、そして本番環境へのリリースまで、Copilot はアプリケーション開発ライフサイクル全体の管理を容易にします。

具体的にはソースコードとそれを動作させるためのDockerfileがあれば、いくつかのコマンド実行と定義ファイルの修正だけでコンテナアプリケーションとして動作させるための一通りのインフラ構築やCI/CDパイプラインの構築など行ってくれるツールです。

# 環境構築
- 実行環境はWindows10
- [リリースノート](https://github.com/aws/copilot-cli/releases)から`copilot-windows.exe`をダウンロードし、`copilot.exe`にリネームしてパスを通すだけです。
- 公式のインストール手順は[こちら](https://aws.github.io/copilot-cli/ja/docs/getting-started/install/)
    - Home brewでのインストールが可能
    - Windows、Mac、Linuxと主要な環境にてバイナリをダウンロードしてインストールも可能

# AWS Copilotに登場する概念
### 主要な概念
| 概念 | 内容 |
| -- | -- |
| [Applications](https://aws.github.io/copilot-cli/ja/docs/concepts/applications/) | その他の概念をまとめる外枠のようなもの |
| [Environments](https://aws.github.io/copilot-cli/ja/docs/concepts/environments/) | 開発環境、検証環境、本番環境といったServiceやJobが動作する環境<br>EnvironmentはVPC単位で分離される |
| [Services](https://aws.github.io/copilot-cli/ja/docs/concepts/services/) | AWS上で動作させたい１サービスを表す<br>Copilotで一番重要な概念です<br>４つタイプが選べるので詳細は後述します |
| [Jobs](https://aws.github.io/copilot-cli/ja/docs/concepts/jobs/) | AWS上で動作させたい1つのバッチジョブを表す<br>具体的にはイベント起動のECSタスク（現時点ではスケジュール実行のみ）<br>今回は触れません |
| [Pipelines](https://aws.github.io/copilot-cli/ja/docs/concepts/pipelines/) | 各EnviromentのService、Jobに対するビルド・デプロイを実現するCI/CDパイプラインを表す<br>具体的にはソースステージ、ビルドステージ、デプロイステージの3層構造のCode Pipeline<br>今回は触れません |

### 概念の関連性
例としてWebアプリとバッチアプリが動作するシステムで、それぞれ開発環境、本番環境があり、各アプリに対するCICDが行える構成の場合におけるイメージ図です。（あくまで例でサンプルアプリではJobとPipelineには触れてません）
![Copilot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942766/f1953fa0-5ea5-04f7-a987-5655164362fd.png)

### Servicesで選択できる４つのタイプについて
| タイプ | 構成 | 用途 |
| --- | --- | --- |
| [Request-Driven Web Service](https://aws.github.io/copilot-cli/ja/docs/concepts/services/#request-driven-web-service) | App Runner | インターネットからアクセス可能なサービスの構築 ※1  |
| [Load Balanced Web Service](https://aws.github.io/copilot-cli/ja/docs/concepts/services/#load-balanced-web-service) | ALB + ECSサービス | インターネットからアクセス可能なサービスの構築 ※1  |
| [Backend Service](https://aws.github.io/copilot-cli/ja/docs/concepts/services/#backend-service) | Service Discovery + ECSサービス | インターナルなアクセスを想定したサービスの構築 |
| [Worker Service](https://aws.github.io/copilot-cli/ja/docs/concepts/services/#worker-service) | SQS + ECSサービス | Pub/Subアーキテクチャにおけるサブスクライバーの構築 |

※1 Request-Driven Web ServiceとLoad Balanced Web Serviceの違いについて
Request-Driven Web Serviceは、App Runner自体がロードバランサ―やオートスケール周りの機能を包括して提供しており手軽にスケーラブルな構成を構築できます。
ただし、WAFが適用できなかったり、カスタマイズできる範囲が限られていたりするので、細かい設定を行いたい場合はLoad Balanced Web ServiceでALB＋ECSサービスの構成の方が向いているかと思います。

# Load Balanced Web Serviceで構築する
### 完成後のインフラ構成図
| ![システム構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942766/00beab9a-5666-d354-6508-acdfd2ac5235.png) |
:--|

api1とapi2は同じSpringアプリをデプロイ、環境変数を読んでHTMLに表示するサンプル実装になってます。
後述のCopilot関連の定義ファイルも含んだサンプル実装は[こちら](https://github.com/tanaka-gxp/copilot-demo)にPushしてあります。

# Application作成
```
copilot app init copilot-demo --resource-tags owner=t.tanaka
```
`--resource-tags`を指定しておくと、Environment、Serviceで作成されるすべてのリソースにも一律タグを付けられるので便利です。

# Environment作成
```
copilot env init --name dev --app copilot-demo --default-config
```
dev環境を作成しています。
この時点でVPC周辺のリソース、ECSクラスターが作成されます。（色々作るので数分かかる）
# Serviceを２つ作成
```
copilot svc init --name api1 --app copilot-demo --dockerfile ./Dockerfile --port 8080 --svc-type "Load Balanced Web Service"
copilot svc init --name api2 --app copilot-demo --dockerfile ./Dockerfile --port 8080 --svc-type "Load Balanced Web Service"
```
api1、api2という名前のServiceを作成しています。
この時点でAWS上にService毎のECRが作成され、ローカルにServiceの詳細を定義するための定義ファイルが生成されます。
`.\copilot\${service名}\manifest.yml`
### manifest.ymlを編集
修正後のapi1のmanifest.yml

```YAML
name: api1
type: Load Balanced Web Service

http:
  path: 'api1'
  healthcheck: '/api1/health'

image:
  build: Dockerfile
  port: 8080

cpu: 256       # ECSタスクに割り当てるCPUユニット
memory: 512    # ECSタスクに割り当てるメモリ
count: 1       # サービスの必要タスク数

network:
  vpc:
    placement: private # タスクをプライベートサブネットに配置する(デフォルトはパブリック)

variables: # 環境変数を定義する
  APP_NAME: api1 # Service毎にHTMLの表示を変えるための環境変数
```

# dev環境へデプロイする
```
copilot svc deploy --name api1 --env dev
copilot svc deploy --name api2 --env dev
```
※Dockerビルドも行われるのでアプリのビルド成果物を取り込む場合は事前にビルドしておくこと。
コマンド毎に下記が行われます。（10分位かかります）

- ローカルでdockerビルドしてECRへプッシュ
- ALB、ターゲットグループ、ECS周りのリソース作成
- タスクが起動し先ほどプッシュしたイメージがデプロイ

# お掃除
```
copilot app delete
```
`Are you sure you want to delete application copilot-demo? [? for help] (Y/n) `と聞かれるので`y`を入力すると`copilot-demo`のApplicationに紐づくEnviroment、Serviceが一式削除されます。

# 使った上で気になった点
### ALBのIP制限に制約がある
manifest.ymlに`http.allowed_source_ips`という設定がありALBに対するIP制限を設定可能だが、これはALBのリスナールールの送信元IPを条件として設定しています。
リスナールールは、IP以外の条件ルールも設定可能だが１つのルールに設定可能な条件は５つまでという制約があり、Copilotでデプロイした場合のデフォルトで、manifest.ymlに定義した`http.path`のルートとルート以降で２つがパス条件として使われ、DNSを設定している場合は更にホストヘッダーも条件として登録されるため、実質２つしかIP条件として設定できないことになります。

下記スクショの赤枠がデフォルトで設定される条件です。
（案件で構築時の設定なのでGithubのサンプルとは差異があります）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942766/30ba06d6-b59c-7173-db0a-8aeb0c8a7b49.png)
単純にIPによるアクセス制限を入れいたいだけであればセキュリティグループで設定したいところですが、私が試したCopilotのバージョン時点ではALBのセキュリティグループに制限を入れる方法はありませんでした。
(案件の時はIPが2つだと足りなかったので手でセキュリティグループに制限を追加しています)

### エラー時はCloudFormationのスタックを見る
manifest.ymlで構文エラーはコンソールに表示されるログから原因がわかるので対応できるのですが、デプロイ先環境にてリソース作成上限にあったってしまうような、実行してみないとわからない類のエラーについては正直コンソールのログだけでは詳細が分からないことが何度かありました。

プライベートサブネットにタスクを配置する構成の場合、パブリックサブネットにNAT Gatewayがデプロイされるのですが、下記はNAT GatewayにアタッチするEIPがリソース作成上限に引っかかってしまった際のログです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942766/d82e0d08-e447-88e3-a3f1-86e394057afe.png)
赤文字のところでエラーになっているのはわかるが、詳細が分かりません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942766/5a67efc3-6875-a0be-0d2b-b3d5e64d5307.png)
CloudFormationのスタックを見てみたところ`The maximum number of addresses has been reached. `と出ており、[この記事](https://aws.amazon.com/jp/premiumsupport/knowledge-center/ec2-troubleshoot-elastic-ip-addresses/)にたどり着き原因がわかりました。
他にも大阪リージョンにデプロイしようとしてCloudMapがリージョンとしてサポートされていなく、デプロイできなかった時などもスタックのログを見ることで大体のエラーの解消出来ました。

### カスタムドメインを設定したい場合はapp init時に指定する必要がある
カスタムドメインを設定する場合は下記の設定が必要です。（公式は[こちら](https://aws.github.io/copilot-cli/ja/docs/developing/domain/)）

1. app init時のパラメータで`--domain ${カスタムドメイン}`を渡して実行
1. manifest.ymlに`http.alias`にカスタムドメインを指定

app init時にドメインの指定をしなかった場合はapp deleteしてinitし直す必要があるようです。
最初manifest.ymlの書き方しか見ていなく作り直すことになりました…
ただ、deleteしてもmanifest.ymlは残り、svc initしても既に同じService名でフォルダがある場合はmanifest.ymlを上書きしないので、そのまま流用して再作成できます。

# まとめ
CloudFormationやTerraformといったIaCツールで１から定義する場合、結構な労力が必要になるので数コマンドで一通りの環境が構築できるのはかなり手軽に感じています。
トラブルシューティングで、AWS全般の知識はある程度必要になるかと思いますが、IaCや手で構築するにしても必要なレベルの知識だと思うのでそこまで大きな問題ではないと思います。
ただ、何度かスクラップ＆ビルドで作り直したりしたのでいくら同じ定義で環境が作れるとしても、本番環境で扱うのはまだちょっとリスクがありそうな印象です。
あと、ALB周りの設定をもう少し弄れるようになってほしいなというのが個人的な感想です。

今回、案件で作った検証環境ではDBが不要だったので触れませんでしたが、[RDS、S3、DynamoDBといったストレージ周りと連携する機能](https://aws.github.io/copilot-cli/ja/docs/developing/additional-aws-resources/)や[CICDを構築する機能](https://aws.github.io/copilot-cli/ja/docs/concepts/pipelines/)などがあり、本格的に使っていくならこの辺りも検証して記事にしていきたいと思います。（きっと…）

# 参考資料
- [公式ドキュメント](https://aws.github.io/copilot-cli/ja/)