【AWS】AWS CLIとECS CLIを使用したECS Webアプリ構築 ~バッチ化まで~

AWSの環境構築をマネジメントコンソールから行うと、画面操作に多くの時間がかかったり、ミスが発生してしまうことが課題だと感じていました。また、手順をドキュメント化するのに掛かるコストも大きいです。

そのため、コマンドラインインターフェース(AWS CLI, ECS CLI)を用いてECSを構築してみます。
また、Windowsバッチ化も行ってみます。(正常系のみ考慮します。)

**バッチを先に確認したい方は、[こちら](#バッチの作成)です。**

# 構成図
![ecs_alb_webapp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166959/42b1f4b5-a242-1a0e-43dd-3e82f2e66971.png)

# 前提条件

- Windows10 環境
- AWS アカウント
- AWS にプログラムからアクセス権限があるユーザー
- 依存ツール
    - [jq](https://stedolan.github.io/jq/)　※バッチファイルのみに使用
    - [Docker Desktop for Windows](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-cliv2.html)
    - [AWS CLI](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ECS_CLI_installation.html) 2.2.18以上
    - ECS CLI (インストール方法の説明あり)

### ツールのバージョン

```bash:jq
jq --version
jq-1.6
```

```bash:Docker
docker -v
Docker version 20.10.7, build f0df350
```

```bash:AWSCLI
aws --version
aws-cli/2.2.27 Python/3.8.8 Windows/10 exe/AMD64 prompt/off
```

```bash:ECSCLI
ecs-cli --version
ecs-cli version 1.21.0 (bb0b8f0)
```

# 環境構築

ECS CLIのインストールから説明します。
※Docker Desktop、AWS CLI、jqのインストールは省略します。

## ECS CLIのインストール

ECS CLIのインストールのみ、**Windows PowerShell** で実行してください。

### Amazon ECS CLI をダウンロード

ECS構築に使用するECS CLIをダウンロードします。

```powershell
New-Item -Path 'C:\Program Files\Amazon\ECSCLI' -ItemType Directory
Invoke-WebRequest -OutFile 'C:\Program Files\Amazon\ECSCLI\ecs-cli.exe' https://amazon-ecs-cli.s3.amazonaws.com/ecs-cli-windows-amd64-latest.exe
```

### GnuPG をダウンロード

[こちら](https://gpg4win.org/download.html)からGnuPGをダウンロードします。

### Amazon ECS PGP パブリックキーを取得

PGP 署名での暗号化のため、パブリックキーを取得します。
参考：https://stackoverflow.com/questions/66217436/gpg-keyserver-receive-failed-no-name

:::note warn
公式ドキュメントではキーサーバーとしてhkp://keys.gnupg.netが指定されていますが、現在は廃止されています。
:::

```powershell
gpg --keyserver keyserver.ubuntu.com --recv BCE9D9A42D51784F
```

### Amazon ECS CLI の署名をダウンロード

署名をダウンロードします。

:::note warn
検証の際にエラー原因となりますので、署名は C 直下にダウンロードしてください。
:::

```powershell
cd /
Invoke-WebRequest -OutFile ecs-cli.asc https://amazon-ecs-cli.s3.amazonaws.com/ecs-cli-windows-amd64-latest.exe.asc
```

### 署名を検証

署名を検証します。

:::note info
出力に警告が表示されることがありますが、問題ありません。
:::

```powershell
gpg --verify ecs-cli.asc 'C:\Program Files\Amazon\ECSCLI\ecs-cli.exe'
```

### 実行アクセス権限をバイナリに適用

バイナリへの実行アクセス権限を適用します。

```powershell
setx path "%path%;C:\Program Files\Amazon\ECSCLI"
```

## インストール確認

※Windows PowerShell を再起動後に実行

```powershell
ecs-cli --version
```

## ECS CLIのインストールでつまづいたところ

- Amazon ECS PGP パブリックキーを取得の際に、ドキュメントで指定されているサーバ(hkp://keys.gnupg.net)が使えない
    - [他のサーバを使用する](https://stackoverflow.com/questions/66217436/gpg-keyserver-receive-failed-no-name)
        - keyserver.ubuntu.com
        - keys.openpgp.org
        - pgp.mit.edu
- Amazon ECS CLI の署名は C 直下にダウンロードして検証しなければいけない

# ソースの作成

```:フォルダ構成
ecs-test
│  docker-compose.yml
│  ecs-params.yml
│  env.cmd
│  task-execution-assume-role.json
│
└─web
    │  app.py
    │  Dockerfile
    │  requirements.txt
    │
    ├─static
    │      style.css
    │
    └─templates
            index.html
```

## Webアプリの作成

### app.py

Pythonのflaskを使ってWebアプリを作成します。
ルートパスで`index.html`を返します。
ポートは5000番に設定します。

```python:app.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html")

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)
```

### requirements.txt

Pythonの依存モジュールを記述するファイルです。
`app.py`でflaskを使用するので記述しておきます。

```text:requirements.txt
Flask==1.1.2
```

### index.html

Webアプリで表示する画面を定義するファイルです。

```html:index.html
<html>
  <head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" type="text/css" href="../static/style.css" />
    <title>Test Page</title>
  </head>
  <section class="wrapper">
    <div class="container">
      <div class="content">
        <h2 class="heading">ECS TEST PAGE</h2>
        <p>Hello World!<br /></p>
      </div>
    </div>
  </section>
</html>

```

### style.css

HTMLファイルのデザインを定義するファイルです。

```css:style.css
/* container */
.wrapper {
  width: 100%;
}
.wrapper .container {
  max-width: 1000px;
  margin: 0px auto;
  padding: 80px 0px;
}
/* content */
.wrapper .content {
  padding: 50px;
  text-align: center;
}
.wrapper .content .heading {
  margin: 0px 0px 40px 0px;
  font-size: 24px;
  font-weight: normal;
  text-align: center;
}

```

## ECS構築に必要な各種定義ファイルの作成

### Dockerfile

Webアプリのコンテナを定義するファイルです。

```dockerfile:Dockerfile
# Python3のイメージを使用
FROM python:3

# Dcokerfileと同じ階層のソースをコンテナイメージのweb配下に追加
ADD . /web
# コマンドの実行ディレクトリをwebに設定
WORKDIR /web

# Pythonのパッケージ管理ツールを最新化
RUN pip install --upgrade pip
# 依存モジュールのインストール
RUN pip install -r requirements.txt

# Webアプリ起動
CMD ["python", "app.py"]
```

### docker-compose.yml

複数のコンテナ定義や起動オプションを設定できるファイルです。(今回は単数です。)

今回はECS構築に使用するので、予めECRリポジトリにイメージを保管しておき、そこからイメージを取得します。そのため、`image`にはECRリポジトリのURIを指定します。
また、`logging`にAWS上でのログ出力先などを指定します。

```yml:docker-compose.yml
version: '3'
services:
  web:
    image: ${ecr_repository_uri}:latest
    ports:
      - '${container_port}:${container_port}'
    logging:
      driver: awslogs
      options:
        awslogs-group: ${logs_group_name}
        awslogs-region: ${region}
        awslogs-stream-prefix: ecs
```

### ecs-params.yml

ECSで起動するコンテナのタスク定義と起動時のネットワーク設定などを記述するファイルです。

```yml:ecs-params.yml
version: 1
task_definition:
  task_execution_role: ${ecs_task_exec_role_name}
  ecs_network_mode: awsvpc
  task_size:
    mem_limit: 0.5GB
    cpu_limit: 256
run_params:
  network_configuration:
    awsvpc_configuration:
      subnets:
        - ${subnet_1}
        - ${subnet_2}
      security_groups:
        - ${security_group_id}
      assign_public_ip: ENABLED

```

### task-execution-assume-role.json

ECSタスク実行ロールを作成する際の信頼ポリシーです。
このファイルを参照してロールの作成を行います。

```json:task-execution-assume-role.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

```

# 環境変数の設定

ECS構築に必要な環境変数を設定します。setから始まる行のイコール(＝)から先を設定してください。
一度に設定できるようにバッチファイルで設定します。以下の表の値を除いて、変更して問題ありません。

| 変数名 | 値 | 理由 |
|:-:|:-:|:-:|
|  container_name | web  |  docker-compose.ymlに依存しているため |
|  container_port |  5000 | Webアプリに依存しているため  |
| src_port  | 80  |  Webアプリとして公開するため |
| dst_port  |  80 |  Webアプリとして公開するため |
|protocol|HTTP|Webアプリとして公開するため|

```:env.cmd
@echo off

rem AWSのアカウントID 12桁(ECRのURIに使用)
set AWS_ACCOUNT_ID=

rem AWS CLIの設定(ECRへのコンテナイメージプッシュに使用)
set AWS_ACCESS_KEY_ID=
set AWS_SECRET_ACCESS_KEY=
set AWS_OUTPUT_FORMAT=json

rem 各リソースを作成するリージョン
set region=ap-northeast-1

rem 各リソースに付けるグループタグ
set tag_group=qiita
rem 各リソースに付ける名前タグ
set tag_name=ecs-test-qiita

rem サービスの命名規則
set service_name=ecsTestQiita

rem ECRリポジトリ名
set ecr_repos_name=ecs-test-qiita-repository

rem Dockerfileのフォルダパス
set DOCKERFILE_PATH=web

rem タスク実行ロール名
set ecs_task_exec_role_name=%service_name%TaskExecutionRole

rem ECSクラスター名
set ecs_cluster_name=%service_name%Cluster

rem ecs-cliの設定(ECSクラスターの作成に使用)
set ecs_cluster_config_name=%service_name%Config
set ecs_profile_name=%service_name%Profile

rem タスク定義名
set ecs_project_name=%service_name%Project

rem ロググループ名
set logs_group_name=/ecs/%service_name%LogsGroup

rem ロードバランサー名(名前は英字で始まる必要があり、小文字、数字、ハイフン (-)、下線 (_)、スラッシュ (/) のみ)
set load_balancer_name=ecs-test-qiita-alb

rem ターゲットグループ名(名前は英字で始まる必要があり、小文字、数字、ハイフン (-)、下線 (_)、スラッシュ (/) のみ)
set target_group_name=ecs-test-qiita-tg

rem コンテナ名=docker-compose.ymlのservicesで定義した名前(ロググループのパスの一部に使用される)
set container_name=web

rem コンテナの接続ポート
set container_port=5000

rem ロードバランサーとセキュリティグループに設定するポート
set src_port=80
set dst_port=80

rem ロードバランサーとターゲットグループに設定する通信プロトコル
set protocol=HTTP
```

# ECS構築

ここからはコマンドプロンプトで実行します。
`ecs-test`フォルダでコマンドプロンプトを開きます。

## 事前準備

`env.cmd`を実行して環境変数を設定します。

```
env.cmd
```

## ECRリポジトリの作成とAWS上にコンテナイメージを保存

ECSでコンテナを実行する際に使用するコンテナイメージを保管するため、ECRリポジトリを作成します。

`aws ecr create-repository`

| オプション | 内容 |必須|
|:-:|:-:|:-:|
| --repository-name  |  リポジトリ名 |〇|
|  --tags | 設定するタグ  |×|

```bash
aws ecr create-repository --repository-name %ecr_repos_name% --tags Key=Name,Value=%tag_name% Key=group,Value=%tag_group%
```

実行結果で出力されたECRリポジトリの URI(`repository.repositoryUri`の値)を環境変数`ecr_repository_uri`に設定します。

```bash
set ecr_repository_uri=<repository.repositoryUri>
```

Dcokerfileを基に、ローカルでコンテナイメージを作成します。
ECRリポジトリにプッシュするため、イメージタグにリポジトリURIを指定します。

`docker build <Dockerfile_path>`

| オプション | 内容 |必須|
|:-:|:-:|:-:|
|-t|イメージタグ|×|


```bash
cd %DOCKERFILE_PATH%
docker build . -t %ecr_repository_uri%
cd ../
```

AWS CLIでECRにログインします。

:::note warn
AWS CLIの設定情報を登録してから実行してください。
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-quickstart.html
:::

`aws ecr get-login-password`

| オプション | 内容 |必須|
|:-:|:-:|:-:|
|--region|ECRのログインパスワードを取得するリージョン|〇|
|--username|ユーザー名(AWSで固定)|〇|

`docker login <login_url>`

| オプション | 内容 |必須|
|:-:|:-:|:-:|
|--password-stdin|ログインパスワードを標準入力で受け取る|〇|

```bash
aws ecr get-login-password --region %region% | docker login --username AWS --password-stdin %AWS_ACCOUNT_ID%.dkr.ecr.%region%.amazonaws.com
```

ECRリポジトリへプッシュします。

`docker push <image_tag>`

```bash
docker push %ecr_repository_uri%
```

## ECSサービスの作成

ECS CLIの設定を作成して、`ecs-cli up`コマンドでECSクラスター、タスク定義、サービス、Cloud Watchロググループを作成します。

### ECSタスク実行ロールを作成

ECSのタスクを実行するために必要なロールを作成します。
ロールの信頼ポリシーは`task-execution-assume-role.json`を指定します。

`aws iam create-role`

| オプション | 内容 |必須|
|:-:|:-:|:-:|
|--role-name|ロール名|〇|
|--assume-role-policy-document|信頼ポリシー|〇|
|--tags|設定するタグ|×|

```bash
aws iam create-role --role-name %ecs_task_exec_role_name% --assume-role-policy-document file://task-execution-assume-role.json --tags Key=Name,Value=%tag_name% Key=group,Value=%tag_group%
```

作成したロールにECSタスク実行権限をアタッチします。
AWSにデフォルトで用意されている`AmazonECSTaskExecutionRolePolicy`を使用します。

`aws iam attach-role-policy`

| オプション | 内容 |必須|
|:-:|:-:|:-:|
|--role-name|ロール名|〇|
|--policy-arn|アタッチするポリシーのARN|〇|

```bash
aws iam attach-role-policy --role-name %ecs_task_exec_role_name% --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
```

### クラスター設定を作成

ECS CLIでECSクラスターを作成する際の構成を設定します。

`ecs-cli configure`

| オプション | 内容 |必須|
|:-:|:-:|:-:|
|--region|リージョン|〇|
|--cluster|クラスター名|〇|
|--default-launch-type|デフォルト起動タイプ(EC2またはFARGATE)|〇|
|--cluster|クラスター名|〇|

```bash
ecs-cli configure --region %region% --cluster %ecs_cluster_name% --default-launch-type FARGATE --config-name %ecs_cluster_config_name%
```

### CLI プロファイルを作成

ECS CLIでコマンドを実行する際の認証情報を作成します。

`ecs-cli configure profile`

| オプション | 内容 |必須|
|:-:|:-:|:-:|
|--access-key|AWSアクセスキー|〇|
|--secret-key|シークレットキー|〇|
|--profile-name|プロファイル名|〇|

```bash
ecs-cli configure profile --access-key %AWS_ACCESS_KEY_ID% --secret-key %AWS_SECRET_ACCESS_KEY% --profile-name %ecs_profile_name%
```

### クラスターの作成

作成したクラスターの設定とプロファイルを使用して、ECSクラスターを作成します。
ECSクラスターをFARGATEで作成すると、VPC・セキュリティグループ・パブリックサブネットx2などが同時に作成されます。

`ecs-cli up`

| オプション | 内容 |必須|
|:-:|:-:|:-:|
|--cluster-config|クラスターの構成名|〇|
|--ecs-profile|ECS CLIのプロファイル名|〇|

```bash
ecs-cli up --cluster-config %ecs_cluster_config_name% --ecs-profile %ecs_profile_name% --tags Name=%tag_name%,group=%tag_group%
```

結果で出力された VPC ID, Subnet IDを環境変数`vpc_id`,`subnet_1`,`subnet_2`に設定します。

```bash
set vpc_id=<VPC_ID>
set subnet_1=<Subnet_ID 1>
set subnet_2=<Subnet_ID 2>
```

### VPCのデフォルトのセキュリティグループIDを取得

VPCに紐づくデフォルトのセキュリティグループIDを取得します。

`aws ec2 describe-security-groups`

| オプション | 内容 |必須|
|:-:|:-:|:-:|
|--filters|Nameの項目をValueで指定|×|

```bash
aws ec2 describe-security-groups --filters Name=vpc-id,Values=%vpc_id%
```

実行結果で出力されたセキュリティグループ ID `SecurityGroups[0].GroupId`を環境変数`security_group_id`に設定します。

```bash
set security_group_id=<SecurityGroups[0].GroupId>
```

### セキュリティグループの設定

セキュリティグループにタグを設定します。(任意)

`aws ec2 create-tags`

| オプション | 内容 |必須|
|:-:|:-:|:-:|
|--resources|タグを設定するリソースID|〇|
|--tags|設定するタグ|〇|

```bash
aws ec2 create-tags --resources %security_group_id% --tags Key=Name,Value=%tag_name% Key=group,Value=%tag_group%
```

自身の IP アドレスを調べます。

```bash
curl https://checkip.amazonaws.com
```

実行結果で出力されたIPアドレスを環境変数`my_ip`に設定します。

```bash
set my_ip=<ip_adress>
```

セキュリティグループのインバウンドルールに自身のIPアドレスを登録します。

`aws ec2 authorize-security-group-ingress`

| オプション | 内容 |必須|
|:-:|:-:|:-:|
|--group-id|セキュリティグループID|×|
|--ip-permissions|IPアドレスのインバウンドルール|×|
|--tag-specifications|タグを設定するリソースタイプとタグの内容|×|

```bash
aws ec2 authorize-security-group-ingress --group-id %security_group_id% --ip-permissions IpProtocol=tcp,FromPort=%src_port%,ToPort=%dst_port%,IpRanges=[{CidrIp=%my_ip%/32,Description=shoma}] --tag-specifications ResourceType=security-group-rule,Tags=[{Key=Name,Value=%tag_name%},{Key=group,Value=%tag_group%}]
```

## ロードバランサーとターゲットグループの作成

ロードバランサーを作成します。

`aws elbv2 create-load-balancer`

| オプション | 内容 |必須|
|:-:|:-:|:-:|
|--name|ロードバランサー名|〇|
|--subnets|接続するサブネット(ALBは2つ以上)|×|
|--security-groups|設定するセキュリティグループID|×|
|--tags|設定するタグ|×|

```bash
aws elbv2 create-load-balancer --name %load_balancer_name% --subnets %subnet_1% %subnet_2% --security-groups %security_group_id% --tags Key=Name,Value=%tag_name% Key=group,Value=%tag_group%
```

実行結果で出力されたロードバランサーの ARN `LoadBalancers[0].LoadBalancerArn`を環境変数`load_balancer_arn`に設定します。

```bash
set load_balancer_arn=<LoadBalancers[0].LoadBalancerArn>
```

ターゲットグループを作成します。

`aws elbv2 create-target-group`

| オプション | 内容 |必須|
|:-:|:-:|:-:|
|--name|ターゲットグループ名|〇|
|--protocol|ターゲットの通信プロトコル|×|
|--port|ターゲットのポート|×|
|--target-type|ターゲットタイプ|×|
|--vpc-id|ターゲットとするVPCのID|×|
|--tags|設定するタグ|×|


```bash
aws elbv2 create-target-group --name %target_group_name% --protocol %protocol% --port %dst_port% --target-type ip --vpc-id %vpc_id% --tags Key=Name,Value=%tag_name% Key=group,Value=%tag_group%
```

実行結果で出力されたターゲットグループの ARN `TargetGroups[0].TargetGroupArn`を環境変数`target_group_arn`に設定します。

```bash
set target_group_arn=<TargetGroups[0].TargetGroupArn>
```

ロードバランサーにリスナーを追加します。
リスナーとは、ロードバランサーに接続するための入り口のことです。

`aws elbv2 create-listener`

| オプション | 内容 |必須|
|:-:|:-:|:-:|
|--load-balancer-arn|リスナーを追加するロードバランサーのARN|〇|
|--protocol|×|
|--port|×|
|--default-actions|〇|

```bash
aws elbv2 create-listener --load-balancer-arn %load_balancer_arn% --protocol %protocol% --port %src_port% --default-actions Type=forward,TargetGroupArn=%target_group_arn%
```

## ECSクラスターにサービスをデプロイ

クラスターにサービスをデプロイします。
同時にロググループも作成されます。

``

| オプション | 内容 |必須|
|:-:|:-:|:-:|
|--project-name|タスク定義とサービスの名前(デフォルトは実行フォルダ名)|×|
|--create-log-groups|構成ファイル(docker-compose.yml)で指定されたロググループを作成|×|
|--cluster-config|ECSクラスター設定名|×|
|--ecs-profile|ECSプロファイル名|×|
|--target-group-arn|関連付けるターゲットグループのARN|×|
|--container-name|コンテナ名|×(※ロードバランサーまたはターゲットグループが指定されている場合は必須)|
|--container-port|コンテナポート|×(※ロードバランサーまたはターゲットグループが指定されている場合は必須)|

```bash
ecs-cli compose --project-name %ecs_project_name% service up --create-log-groups --cluster-config %ecs_cluster_config_name% --ecs-profile %ecs_profile_name% --target-group-arn %target_group_arn% --container-name %container_name% --container-port %container_port%
```

### サービスの接続先情報確認

エンドポイントであるロードバランサーの接続先情報を確認します。

`aws elbv2 describe-load-balancers`

| オプション | 内容 |必須|
|:-:|:-:|:-:|
|--load-balancer-arns|対象ロードバランサーのARN|×|

```bash
aws elbv2 describe-load-balancers --load-balancer-arns %load_balancer_arn%
```

出力結果の`LoadBalancers[0].DNSName`が接続先ですので、アクセスして確認してみましょう。

## ECS構築でつまづいたところ

- ECR リポジトリの名前は大文字不可
    - 名前は英字で始まる必要があり、小文字、数字、ハイフン (-)、下線 (_)、スラッシュ (/) のみ(参考：https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/repository-create.html)
- セキュリティグループのインバウンドルールに登録するIP アドレスはレンジが必要(/32)
- AWS CLI のバージョンによってはセキュリティグループにタグ付けするオプション`--tag-specifications`がバージョンによっては使用できない(2021/7/7にリリースされた2.2.18からの機能のため)

# バッチの作成

```diff:フォルダ構成
ecs-test
+│  create.cmd
+│  delete.cmd
 │  docker-compose.yml
 │  ecs-params.yml
 │  env.cmd
 │  task-execution-assume-role.json
 │
 └─web
     │  app.py
     │  Dockerfile
     │  requirements.txt
     │
     ├─static
     │      style.css
     │
     └─templates
             index.html
```

## 構築バッチ

これまで使用したコマンドを`create.cmd`に集約します。

手動実行との差異

- AWSの認証情報を標準入力から受け取り
- 各ツールのインストール確認(バージョンは未考慮)
- リソースのプロパティの一部は`resource_properties.cmd`に書き込み(削除時にも使用するため)
- ロガーで各コマンドの実行時刻を標準出力


```batchfile:create.cmd
rem 実行したコマンドを標準出力しない
@echo off
rem 遅延関数(!example!)を有効化
setlocal enabledelayedexpansion
rem カレントディレクトリに移動(/dはドライブ変更可能に設定するオプション)
cd /d %~dp0

WHERE /Q jq
if not %ERRORLEVEL% == 0 (
  call :loggerError "Please install jq : https://stedolan.github.io/jq/"
  cmd /k
)

WHERE /Q aws
if not %ERRORLEVEL% == 0 (
  call :loggerError "Please install AWS CLI : https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-cliv2.html"
  cmd /k
)

WHERE /Q ecs-cli
if not %ERRORLEVEL% == 0 (
  call :loggerError "Please install ECS CLI : https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ECS_CLI_installation.html"
  cmd /k
)

WHERE /Q docker
if not %ERRORLEVEL% == 0 (
  call :loggerError "Please install Docker Desktop for Windows : https://docs.docker.jp/docker-for-windows/install.html"
  cmd /k
)

rem 環境変数を設定
call :loggerInfo "set enviroment"
set /p AWS_ACCOUNT_ID="AWS_ACCOUNT_ID: "
set /p AWS_ACCESS_KEY_ID="AWS_ACCESS_KEY_ID: "
set /p AWS_SECRET_ACCESS_KEY="AWS_SECRET_ACCESS_KEY: "
call env.cmd

rem リソースのプロパティファイル存在確認
call :loggerInfo "check properties file"
if not exist %resources_properties_file% type nul > %resources_properties_file%
echo rem START at %date% %time%>>%resources_properties_file%

rem AWS CLIの設定
call :loggerInfo "set AWS CLI configure"
(
  echo %AWS_ACCESS_KEY_ID%
  echo %AWS_SECRET_ACCESS_KEY%
  echo %region%
  echo %AWS_OUTPUT_FORMAT%
) | aws configure > nul

rem コンテナイメージを格納するECRリポジトリを作成
call :loggerInfo "create ECR repository"
for /f "usebackq" %%t in (`"aws ecr create-repository --repository-name %ecr_repos_name% --tags Key=Name,Value=%tag_name% Key=group,Value=%tag_group% " ^| jq .repository.repositoryUri`) do (
  set ecr_repository_uri=%%t
  rem ダブルクォーテーションを削除
  call set ecr_repository_uri=%%ecr_repository_uri:"=%%

  rem プロパティファイルに追記
  echo set ecr_repository_uri=!ecr_repository_uri!>>%resources_properties_file%
)

rem コンテナイメージの作成
call :loggerInfo "build container image"
cd /d %~dp0
cd %DOCKERFILE_PATH%
docker build . -t %ecr_repository_uri% --no-cache
cd /d %~dp0

rem ECR へプッシュ
call :loggerInfo "push image to ECR repository"
aws ecr get-login-password --region %region% | docker login --username AWS --password-stdin %AWS_ACCOUNT_ID%.dkr.ecr.%region%.amazonaws.com
docker push %ecr_repository_uri%:latest

rem タスク実行ロールを作成(task-execution-assume-role.jsonを作成後に実行)
call :loggerInfo "create task execution role"
aws iam create-role --role-name %ecs_task_exec_role_name% --assume-role-policy-document file://task-execution-assume-role.json --tags Key=Name,Value=%tag_name% Key=group,Value=%tag_group% > nul

rem タスク実行ロールのポリシーをアタッチ
call :loggerInfo "attach role"
aws iam attach-role-policy --role-name %ecs_task_exec_role_name% --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

rem クラスター設定を作成
call :loggerInfo "create ECS Cluster settings"
ecs-cli configure --region %region% --cluster %ecs_cluster_name% --default-launch-type FARGATE --config-name %ecs_cluster_config_name%

rem アクセスキーとシークレットキーを使用して CLI プロファイルを作成
call :loggerInfo "create ecs-cli profile"
ecs-cli configure profile --access-key %AWS_ACCESS_KEY_ID% --secret-key %AWS_SECRET_ACCESS_KEY% --profile-name %ecs_profile_name%

rem Amazon ECS クラスターを作成, デフォルトVPCのIDを取得
call :loggerInfo "create ECS Cluster and get default VPC ID"
for /f "tokens=1,2* usebackq delims=^:" %%i in (`"ecs-cli up --cluster-config %ecs_cluster_config_name% --ecs-profile %ecs_profile_name% --tags Name=%tag_name%,group=%tag_group% " ^| findstr /r "created$"`) DO (
  echo %%i | find "VPC" > nul
  if not ERRORLEVEL 1 (
    set vpc_id=%%j
    call set vpc_id=%%vpc_id: =%%
    echo VPC ID: !vpc_id!

    rem プロパティファイルに追記
    echo set vpc_id=!vpc_id!>>%resources_properties_file%
  )

  echo %%i | find "Subnet" > nul
  if not ERRORLEVEL 1 if not defined subnet_1 (
    set subnet_1=%%j
    call set subnet_1=%%subnet_1: =%%
    echo Subnet ID 1: !subnet_1!

    rem プロパティファイルに追記
    echo set subnet_1=!subnet_1!>>%resources_properties_file%

  ) else if not defined subnet_2 (
    set subnet_2=%%j
    call set subnet_2=%%subnet_2: =%%
    echo Subnet ID 2: !subnet_2!

    rem プロパティファイルに追記
    echo set subnet_2=!subnet_2!>>%resources_properties_file%
  )
)

rem セキュリティグループIDを取得
call :loggerInfo "get Security Group ID"
for /f "usebackq" %%t in (`"aws ec2 describe-security-groups --filters Name=vpc-id,Values=%vpc_id% " ^| jq .SecurityGroups[0].GroupId`) do (
  set security_group_id=%%t
  rem ダブルクォーテーションを削除
  call set security_group_id=%%security_group_id:"=%%

  rem プロパティファイルに追記
  echo set security_group_id=!security_group_id!>>%resources_properties_file%
)

rem セキュリティグループにタグ付け
call :loggerInfo "attach tags to Security Group"
aws ec2 create-tags --resources %security_group_id% --tags Key=Name,Value=%tag_name% Key=group,Value=%tag_group%

rem 自身のIPアドレスを取得
call :loggerInfo "get own IP Address"
for /f "usebackq" %%t in (`curl https://checkip.amazonaws.com`) do (
  set my_ip=%%t
)

rem セキュリティグループに自身のIPアドレスを登録
call :loggerInfo "register IP Address to Security Group"
aws ec2 authorize-security-group-ingress --group-id %security_group_id% --ip-permissions IpProtocol=tcp,FromPort=%src_port%,ToPort=%dst_port%,IpRanges=[{CidrIp=%my_ip%/32,Description=shoma}] --tag-specifications ResourceType=security-group-rule,Tags=[{Key=Name,Value=%tag_name%},{Key=group,Value=%tag_group%}]

rem ロードバランサーの作成
call :loggerInfo "create Application Load Balancer"
for /f "usebackq" %%x in (`"aws elbv2 create-load-balancer --name %load_balancer_name% --subnets %subnet_1% %subnet_2% --security-groups %security_group_id% --tags Key=Name,Value=%tag_name% Key=group,Value=%tag_group% " ^| jq .LoadBalancers[0].LoadBalancerArn`) do (
  set load_balancer_arn=%%x
  rem ダブルクォーテーションを削除
  call set load_balancer_arn=%%load_balancer_arn:"=%%

  rem プロパティファイルに追記
  echo set load_balancer_arn=!load_balancer_arn!>>%resources_properties_file%
)

rem ターゲットグループの作成
call :loggerInfo "create Target Group"
for /f "usebackq" %%x in (`"aws elbv2 create-target-group --name %target_group_name% --protocol %protocol% --port %dst_port% --target-type ip --vpc-id %vpc_id% --tags Key=Name,Value=%tag_name% Key=group,Value=%tag_group% " ^| jq .TargetGroups[0].TargetGroupArn`) do (
  set target_group_arn=%%x
  rem ダブルクォーテーションを削除
  call set target_group_arn=%%target_group_arn:"=%%

  rem プロパティファイルに追記
  echo set target_group_arn=!target_group_arn!>>%resources_properties_file%
)

rem ロードバランサーにリスナーを追加
call :loggerInfo "add listener to Application Load Balancer"
aws elbv2 create-listener --load-balancer-arn %load_balancer_arn% --protocol %protocol% --port %src_port% --default-actions Type=forward,TargetGroupArn=%target_group_arn%

rem クラスターにサービスをデプロイ
call :loggerInfo "deploy ECS Services"
ecs-cli compose --project-name service up %ecs_project_name% --create-log-groups --cluster-config %ecs_cluster_config_name% --ecs-profile %ecs_profile_name% --target-group-arn %target_group_arn% --container-name %container_name% --container-port %container_port%

rem クラスターの確認
call :loggerInfo "check ECS Cluster"
ecs-cli compose --project-name service ps %ecs_project_name% --cluster-config %ecs_cluster_config_name% --ecs-profile %ecs_profile_name%

call :loggerInfo "finish build and deploy Services"

call :loggerInfo "get DNS"
for /f "usebackq" %%x in (`"aws elbv2 describe-load-balancers --load-balancer-arns %load_balancer_arn% " ^| jq .LoadBalancers[0].DNSName`) do (
  set load_balancer_dns=%%x
  rem ダブルクォーテーションを削除
  call set load_balancer_dns=%%load_balancer_dns:"=%%

  rem プロパティファイルに追記
  echo set load_balancer_dns=!load_balancer_dns!>>%resources_properties_file%

  rem コンソールに表示
  echo ALB DNS is !load_balancer_dns!
)

echo rem FINISH at %date% %time%>>%resources_properties_file%

cmd /k

:loggerInfo
echo %date% %time% [INFO] %~1
exit /b

:loggerError
echo %date% %time% [ERROR] %~1
exit /b
```

## 削除バッチ

削除バッチは、構築バッチ実行時に作成された`resource_properties.cmd`を使用します。
それを基に削除するリソースを指定します。

```batchfile:delete.cmd
rem 実行したコマンドを標準出力しない
@echo off
rem カレントディレクトリに移動(/dはドライブ変更可能に設定するオプション)
cd /d %~dp0

rem 環境変数を設定
call env.cmd

if not exist %resources_properties_file% (
  call :loggerError "NOT EXIST %resources_properties_file%"
  cmd /k
)

call %resources_properties_file%

rem ロードバランサーの削除
call :loggerInfo "delete Application Load Balancer"
aws elbv2 delete-load-balancer --load-balancer-arn %load_balancer_arn%

rem ロードバランサー削除待ち
call :loggerInfo "wait 10 seconds"
timeout 10 > nul

rem ターゲットグループの削除
call :loggerInfo "delete Target Group"
aws elbv2 delete-target-group --target-group-arn %target_group_arn%

rem クラスターのサービスを削除
call :loggerInfo "delete servise on ECS Cluster"
ecs-cli compose --project-name %ecs_project_name% service down --cluster-config %ecs_cluster_config_name% --ecs-profile %ecs_profile_name%

rem クラスターを削除
call :loggerInfo "delete ECS Cluster"
ecs-cli down --force --cluster-config %ecs_cluster_config_name% --ecs-profile %ecs_profile_name%

rem ECRリポジトリの削除
call :loggerInfo "delete ECR repository"
aws ecr delete-repository --repository-name %ecr_repos_name% --force

rem ロググループを削除
call :loggerInfo "delete Logs Group"
aws logs delete-log-group --log-group-name %logs_group_name%

rem ECSタスク実行ロールの削除
call :loggerInfo "delete task execution role"
aws iam detach-role-policy --role-name %ecs_task_exec_role_name% --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
aws iam --region %region% delete-role --role-name %ecs_task_exec_role_name%

call :loggerInfo "finish delete Services"

cmd /k

:loggerInfo
echo %date% %time% [INFO] %~1
exit /b

:loggerError
echo %date% %time% [ERROR] %~1
exit /b
```

# おわりに

コマンドのみでECSにWebアプリを構築してバッチ化まで行いました。

依存ツールが多いことやWindowsバッチが書きにくいと感じたため、次はLinuxベースのDockerコンテナで環境構築を行って、shellスクリプトで作成するのが良いと考えています。

また、バッチのようにコードベースで作成することに加えて、異常系を考慮したいと思いました。[AWS CloudFormation](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/Welcome.html)や[Terraform](https://www.terraform.io/)など、IaC(Infrastructure as Code)ツールがありますので、そちらで実現してみたいと考えています。

# 参考

[Amazon ECS CLI のインストール](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ECS_CLI_installation.html)
[Amazon ECS CLI の設定](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ECS_CLI_Configuration.html)
[gpg: keyserver receive failed: No name - stack overflow](https://stackoverflow.com/questions/66217436/gpg-keyserver-receive-failed-no-name)
[チュートリアル: Amazon ECS CLI を使用して Fargate タスクのクラスターを作成する](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-cli-tutorial-fargate.html)
[MSI インストーラを使用した Windows での AWS CLI バージョン 2 のインストールまたは更新](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-cliv2-windows.html#cliv2-windows-install)
[Amazon EC2、VPC セキュリティグループルールにリソース識別子とタグを追加](https://aws.amazon.com/jp/about-aws/whats-new/2021/07/amazon-ec2-adds-resource-identifiers-tags-vpc-security-groups-rules/)
[CodeDeploy Amazon ECS デプロイ用のロードバランサー、ターゲットグループ、リスナーのセットアップ](https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/deployment-groups-create-load-balancer-for-ecs.html)