個人開発のインフラをEC2からFargateに置き換えました！

## 概要

Qiitaのストックを整理するためのサービス「[Mindexer(ミンデクサー)](https://www.mindexer.net/)」のインフラをEC2からFargateに置き換えました。その際に得られたTipsをまとめた記事となります。

AWSの環境は全てTerraformで構築しています。開発環境のみECSのEC2起動モードも利用しています。

ソースコードは、こちらで公開しています。
https://github.com/nekochans/qiita-stocker-terraform

なお、ECSのスペックについては本番を想定した設定となっていませんのでご注意ください。

## サービスについて
[AWS + Laravel + Vue.js でQiitaのストックを整理するサービスを作りました！【個人開発】](https://qiita.com/kobayashi-m42/items/14e137727ffda3bf79e7)
にて解説ししていますので、こちらも合わせて見ていただけますと幸いです😊
バックエンド、フロントエンドの技術についてもソースコード付きで解説しています。

## 全体の構成
実際の構成はTerraformのプロジェクトをご確認いただくのが早いと思いますが、ここでは設定のポイント等を解説したいと思います。

- ECR
- CodeBuild
- ECS(EC2インスタンスを利用)
  - Laravel/Nginx
- Fargate
  - Laravel/Nginx

### ECR
ECS(EC2)、Fargate共に利用しています。
CodeBuildプロジェクトを作成し、ECRへのプッシュを行なっています。(最終的には、CodePipelineを利用したFargateへのデプロイを構築する予定。)

**ECRのライフサイクルポリシー**
ライフサイクルポリシーを設定しておくことで不要になったイメージを自動で削除することができます。
ECRには月500MBのストレージの無料利用枠が用意されていますが、これ以上となると利用料が発生するため、ライフサイクルを設定しておくといいかと思います。

[AWSドキュメント Amazon ECR ライフサイクルポリシー](https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/LifecyclePolicies.html)

下記は、イメージを5つまで保持し、6つめ以上の古いイメージは自動で削除される設定です。

```
locals {
  lifecycle_policy = <<EOF
  {
    "rules": [
      {
        "rulePriority": 10,
        "description": "Expire images count more than 5",
        "selection": {
          "tagStatus": "any",
          "countType": "imageCountMoreThan",
          "countNumber": 5
        },
        "action": {
          "type": "expire"
        }
      }
    ]
  }
EOF
}
```

### ECS(EC2インスタンス)
開発環境のみで使用しています。
FargateではコンテナホストにSSHするこができないため、開発中にSSHでコンテナの動作確認等を行いたい場合などに利用しています。

**EC2インスタンスをECSのクラスタに所属させる設定**
デフォルトでは、EC2のコンテナインスタンスはデフォルトのクラスタで起動されます。
そのため、デフォルト以外のクラスタで起動するには、クラスタを指定する必要があります。
この設定が、Terraformで構築している際にわかりにくかったので、記載しておきます。

EC2ユーザーデータを使用して、コンテナインスタンスの環境変数を設定します。
ここでは、`ECS_CLUSTER`にコンテナ名を指定しています。

> ECS_CLUSTER
このエージェントが確認するクラスター。この値を定義しない場合、default クラスターが想定されます。default クラスターが存在しない場合は、Amazon ECS コンテナエージェントによってその作成が試みられます。default 以外のクラスターを指定した場合、そのクラスターが存在しないと、登録は失敗します。

参考：[AWSドキュメント Amazon ECS コンテナエージェントの設定](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-agent-config.html)

```:userdata.sh
#!/bin/bash

cat << EOT >> /etc/ecs/ecs.config
ECS_CLUSTER=${cluster_name}
EOT
```

```:ecs.tf
data "template_file" "user_data" {
  template = "${file("../../../../modules/aws/api/user-data/userdata.sh")}"

  vars {
    cluster_name = "${aws_ecs_cluster.api_ecs_cluster.name}"
  }
}

resource "aws_instance" "ecs_instance" {
 // その他の設定は省略
 user_data = "${data.template_file.user_data.rendered}"
}
```

### Fargate
Fargateは本番での運用を想定し、Green/Blueデプロイの設定も行なっています。
Terraformでの構築においてECS(EC2)との設定の大きな違いは、EC2インスタンスの設定が不要なところです。
(上記で設定した、コンテナインスタンスの環境変数の設定ももちろん不要)

ここでは、Blue/Greenデプロイについて解説します。
マネジメントコンソールからの設定については、[AWS FargateでBlue/Greenデプロイを行う](https://qiita.com/kobayashi-m42/items/59cfe693670d930f7522)という記事の中で解説しております。

- ターゲットグループの設定

ターゲットを切り替えることでBlue/Greenデプロイを実行するため、`fargate_api_blue`,`fargate_api_green`の2つのターゲットグループを作成します。


```alb.tf
resource "aws_alb_target_group" "fargate_api_blue" {
  name     = "${lookup(var.fargate, "${terraform.env}.name", var.fargate["default.name"])}-blue"
  port     = 80
  protocol = "HTTP"
  vpc_id   = "${lookup(var.vpc, "vpc_id")}"

  health_check {
    path                = "/api/statuses"
    timeout             = 5
    healthy_threshold   = 5
    unhealthy_threshold = 2
    interval            = 20
    matcher             = 200
  }

  target_type = "ip"
}

resource "aws_alb_target_group" "fargate_api_green" {
  name     = "${lookup(var.fargate, "${terraform.env}.name", var.fargate["default.name"])}-green"
  port     = 80
  protocol = "HTTP"
  vpc_id   = "${lookup(var.vpc, "vpc_id")}"

  health_check {
    path                = "/api/statuses"
    timeout             = 5
    healthy_threshold   = 5
    unhealthy_threshold = 2
    interval            = 20
    matcher             = 200
  }

  target_type = "ip"
}
```

- CodeDeployアプリケーションとデプロイメントグループを作成

```codedeploy.tf

resource "aws_codedeploy_app" "fargate_api" {
  compute_platform = "ECS"
  name             = "${lookup(var.fargate, "${terraform.env}.name", var.fargate["default.name"])}"
}

resource "aws_codedeploy_deployment_group" "fargate_api_blue_green_deploy" {
  app_name               = "${aws_codedeploy_app.fargate_api.name}"
  deployment_group_name  = "blue-green"
  service_role_arn       = "${aws_iam_role.codedeploy_for_fargate_role.arn}"
  deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"

  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_FAILURE"]
  }

  blue_green_deployment_config {
    deployment_ready_option {
      action_on_timeout = "CONTINUE_DEPLOYMENT"
    }

    terminate_blue_instances_on_deployment_success {
      action                           = "TERMINATE"
      termination_wait_time_in_minutes = "1"
    }
  }

  deployment_style {
    deployment_option = "WITH_TRAFFIC_CONTROL"
    deployment_type   = "BLUE_GREEN"
  }

  ecs_service {
    cluster_name = "${aws_ecs_cluster.api_fargate_cluster.name}"
    service_name = "${aws_ecs_service.api_fargate_service.name}"
  }

  load_balancer_info {
    target_group_pair_info {
      prod_traffic_route {
        listener_arns = ["${aws_alb_listener.fargate_alb.arn}"]
      }

      target_group {
        name = "${aws_alb_target_group.fargate_api_blue.name}"
      }

      target_group {
        name = "${aws_alb_target_group.fargate_api_green.name}"
      }
    }
  }
}
```

IAMロールの設定などは割愛していますが、簡単にBlue/Greenデプロイの設定ができるのでとても便利ですね！

### NginxのDockerfile

ECS(EC2)とFargateではコンテナ間の通信方法が異なります。

- ECS(EC2)：タスクの定義で`link`パラメータを設定しコンテナ間の通信を許可する
- Fargate：同じタスクに属するコンテナは、localhost+ポート番号で通信する

上記の内容を踏まえて、Nginxの設定ファイルをECS(EC2)とFargateの両方で使用できるよう作成します。

テンプレート`default.conf.template`を作成し、`PHP_HOST`を切り替え可能とします。

```:docker/nginx/config/default.conf.template
  location ~ \.php$ {
    fastcgi_pass ${PHP_HOST}:9000;
  }
```

```:Dockerfile
FROM nginx:1.15.5-alpine

ENV PHP_HOST=127.0.0.1

ADD ./docker/nginx/config/default.conf.template /etc/nginx/conf.d/default.conf.template

RUN mkdir -p /var/www/html/public
ADD ./public/ /var/www/html/public

CMD /bin/sh -c 'sed "s/\${PHP_HOST}/$PHP_HOST/" /etc/nginx/conf.d/default.conf.template  > /etc/nginx/conf.d/default.conf && nginx -g "daemon off;"'
```

ECS(EC2)のタスクの定義の中で、`default.conf`に設定するための環境変数を設定します。
(Fargateのタスクの定義では環境変数の設定を行わないので、Dockerfileで設定している `PHP_HOST=127.0.0.1`が設定される)


```:modules/aws/api/task/ecs-api.json

    "links": ["php"],
    "environment": [
      {
        "name": "PHP_HOST",
        "value": "php"
      }
  :
```

### コンテナの環境変数
タスクの定義の環境変数は、パラメータストアから取得しています。プログラムに機密情報を保持する必要がなくなるというメリットがあります。
設定方法は下記の通りです。

1. マネジメントコンソールからAWS Secrets Managerに機密情報を登録
2. AWS Secrets ManagerからParameter StoreをTerraformで作成
3. タスクの定義のParameter StoreのAmazon リソースネーム (ARN)をタスクの定義に設置する

`2. AWS Secrets ManagerからParameter StoreをTerraformで作成`の解説については、下記の記事をご確認ください。
[AWS Secrets ManagerからParameter StoreをTerraformで作成する](https://qiita.com/keitakn/items/55da7f9f3c3659cfc804)

## TerraformでECSを管理する際の注意点

Terraformで作成したタスクの定義がECSサービスで動作している状態でTerraformでタスクの定義を変更すると、現在動いているタスクの定義を削除してから新しいリビジョンのタスクの定義が作成されます。
サービス自体は停止しませんが、直前のタスクの定義のリビジョンが削除されてしまうのでご注意ください。

## 今後の課題
以下についても、今後対応する予定です。

- デプロイの自動化
現時点では、CodeBuildでECRへプッシュするところまでしか自動化することができていません。
今後は、CodePipelineを使って、GitHubにプッシュされたら自動でデプロイが実行されるまでの仕組みを構築しようと思っています。

- Datadogを利用したコンテナの監視
Amazon ECS CloudWatch のメトリクスではクラスタ、およびクラスタ内のサービス単位のメトリクスはサポートされていますが、タスク単位のメトリクスはサポートされていません。
Datadogを利用して、タスク単位でリソースを監視できるようにしていきたいと思っています。

[AWS ドキュメント　Amazon ECS CloudWatch のメトリクス](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/cloudwatch-metrics.html#available_cloudwatch_metrics)
[Datadog Docs ECS Fargate](https://docs.datadoghq.com/integrations/ecs_fargate/)