Amazon ECSでRailsアプリケーションを運用するのに行ってること

### Terraform

AWSのインフラ構成はTerraform管理してる.

#### tfstateを分割する

tfstateが1つのままだと、Terraformのresourceを増やしていったときに

* 頻繁に更新するresourceとそうでもないものがある
* 適応するのに時間が掛かる
* エラーの切り分けしずらくなる

ということからtfstateを分割してる。
ただ分割しすぎると、適応漏れや適応順番が複雑になるので2つに分割してる。

```
.
├── environments
│   ├── immutable
│   │   ├── backend.tf
│   │   ├── main.tf
│   │   ├── provider.tf
│   │   └── variable.tf
│   └── mutable
│       ├── backend.tf
│       ├── main.tf
│       ├── output.tf
│       ├── provider.tf
│       └── variable.tf
└── modules
    ├── alb
    ├── ecr
    ├── ecs
    ├── iam
    ├── rds
    ├── s3
    ├── sns
    ├── ssm
    └── vpc
```

#### immutableで実行してるもの

* ecs

#### mutableで実行してるもの

* alb
* ecr
* ecs
* iam
* rds
* s3
* ssm
* sns
* vpc

### tfstateはS3で管理する

複数人でTerraformを扱う場合、別の人がTerraformを先に実行していてtfstateの内容が変わっていた場合、そのtfstateを取得したうえで、Terraform実行しないと壊れてしまう。

そういったことが起こるのでtfstateはS3で管理してる。

```:environments/mutable/backend.tf
terraform {
  backend "s3" {
    bucket = "kptboard-terraform"
    key    = "mutable/terraform.tfstate"
    region = "ap-northeast-1"
  }
}
```

```:environments/immutable/backend.tf
terraform {
  backend "s3" {
    bucket = "kptboard-terraform"
    key    = "immutable/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

data "terraform_remote_state" "mutable" {
  backend = "s3"
  config {
    bucket = "kptboard-terraform"
    key    = "env:/${terraform.workspace}/mutable/terraform.tfstate"
    region = "ap-northeast-1"
  }
}
```

## セキュリティ

この資料が参考になる。
https://speakerdeck.com/pottava/container-security-20180310

## ログ

CloudWatch Logsを使うのが簡単に設定できるとこもあり、これを使ってる。
CloudWatch Logsはアーカイブに対して課金されるので、ログをずっと残す場合はFluentdなどでS3に転送したほうが良いと思う。

### Terraform

AWSLogsを使ってる

```:aws_cloudwatch_log_group.tf
resource "aws_cloudwatch_log_group" "kptboard" {
  name = "${var.kptboard}"
}
```

### ECSタスク定義

```:kptboard.json.tpl
"logConfiguration": {
  "logDriver": "awslogs",
    "options": {
      "awslogs-group": "${kptboard}",
      "awslogs-region": "${aws_region}"
    }
  }
}
```

## 監視

CloudWatchのアラームを使ってる。

https://dev.classmethod.jp/cloud/aws/amazon-ecs-cloudwatch-metrics/
> ServiceのCPU/MemoryUtilizationは、100%を越えることがある ことには注意が必要です。Dockerの仕組みとして、起動しているホストOS上に、別のコンテナによって確保されていないCPU/メモリ領域がある場合は、自分で確保しているCPU/メモリ量以上にリソースを利用することができるのです。 

という理由から100%を超えることがあるため、監視のしきい値を適切に設定するのが難しく、動いてるコンテナ全体でみないと判断しずらいため、CPUとメモリーの監視は特定のコンテナではなくホストのを監視している。

コンテナの方はECSサービスをALBのヘルスチェックが通ったかどうかで監視してる。
ヘルスチェックが急に通らなくなるのは、コンテナが何らかの理由で落ちた場合が多かった。

### Terraform

```:aws_cloudwatch_metric_alarm.tf
resource "aws_cloudwatch_metric_alarm" "kptboard_cpu" {
  alarm_name = "${var.kptboard}-cpu"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods = "1"
  metric_name = "CPUUtilization"
  namespace = "AWS/ECS"
  period = "300"
  statistic = "Average"
  threshold = "80"
  dimensions {
    ClusterName = "${var.kptboard}"
  }
  ok_actions = ["${var.aws_cloudwatch_metric_alarm_action_arn}"]
  alarm_actions = ["${var.aws_cloudwatch_metric_alarm_action_arn}"]
  insufficient_data_actions = ["${var.aws_cloudwatch_metric_alarm_action_arn}"]
}

resource "aws_cloudwatch_metric_alarm" "kptboard_memory" {
  alarm_name = "${var.kptboard}-memory"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods = "1"
  metric_name = "MemoryUtilization"
  namespace = "AWS/ECS"
  period = "300"
  statistic = "Average"
  threshold = "80"
  dimensions {
    ClusterName = "${var.kptboard}"
  }
  ok_actions = ["${var.aws_cloudwatch_metric_alarm_action_arn}"]
  alarm_actions = ["${var.aws_cloudwatch_metric_alarm_action_arn}"]
  insufficient_data_actions = ["${var.aws_cloudwatch_metric_alarm_action_arn}"]
}

resource "aws_cloudwatch_metric_alarm" "app_server_healthy_host_count" {
  alarm_name = "${var.kptboard}-app-server-healthy-host-count"
  comparison_operator = "LessThanThreshold"
  evaluation_periods = "1"
  metric_name = "HealthyHostCount"
  namespace = "AWS/ApplicationELB"
  period = "180"
  statistic = "Average"
  threshold = "1"
  dimensions {
    LoadBalancer = "${var.aws_lb_kptboard_arn_suffix}"
    TargetGroup = "${var.aws_lb_target_group_attachment_server_arn_suffix}"
  }
  ok_actions = ["${var.aws_cloudwatch_metric_alarm_action_arn}"]
  alarm_actions = ["${var.aws_cloudwatch_metric_alarm_action_arn}"]
  insufficient_data_actions = ["${var.aws_cloudwatch_metric_alarm_action_arn}"]
}

resource "aws_cloudwatch_metric_alarm" "app_server_un_healthy_host_count" {
  alarm_name = "${var.kptboard}-app-server-un-healthy-host-count"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods = "1"
  metric_name = "UnHealthyHostCount"
  namespace = "AWS/ApplicationELB"
  period = "180"
  statistic = "Average"
  threshold = "0"
  dimensions {
    LoadBalancer = "${var.aws_lb_kptboard_arn_suffix}"
    TargetGroup = "${var.aws_lb_target_group_attachment_server_arn_suffix}"
  }
  ok_actions = ["${var.aws_cloudwatch_metric_alarm_action_arn}"]
  alarm_actions = ["${var.aws_cloudwatch_metric_alarm_action_arn}"]
  insufficient_data_actions = ["${var.aws_cloudwatch_metric_alarm_action_arn}"]
}
```

## コンテナで使う環境変数

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_definition_parameters.html

> 認証情報データなどの機密情報にプレーンテキストの環境変数を使用することはお勧めしません。

コンテナで使う環境変数をECSタスク定義に入れるのは推奨されてないため、AWS Systems Manager パラメータストアを使って管理している。AWS Systems Manager パラメータストアはKMSを使った暗号化に対応している。

最近AWS Secrets Managerが使えるようになったのでこちらが試してよければ移行する可能性はある。

### ECSタスク定義

```:kptboard.json.tpl
"environment": [
  {
    "name": "AWS_DEFAULT_REGION",
    "value": "${aws_region}"
  },
  {
    "name": "PARAMETER_STORE_PREFIX",
    "value": "${kptboard}"
  }
]
```

### Terraform

```:modules/ssm/aws_ssm_parameter.tf
resource "aws_ssm_parameter" "kptboard_app_server_secret_key_base" {
  name  = "${var.kptboard}-rails.secret.key.base"
  type  = "SecureString"
  value = "${var.kptboard_app_server_secret_key_base}"
  overwrite = true
}
```

```:modules/iam/policies/kptboard_ssm_policy.json.tpl
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssm:GetParameters"
      ],
      "Resource": [
        "arn:aws:ssm:${region}:${account_id}:parameter/${kptboard}-*.*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt"
      ],
      "Resource": "arn:aws:kms:us-east-1:${account_id}:key/alias/aws/ssm"
    }
  ]
}
```

### Dockerfile

docker-entrypoint.shでPARAMETER_STORE_PREFIXが設定されてる場合はパラメータストアから取得するようにしてる。
こうすることでパラメータストアが使えない環境では実行しないようにできる。

```:docker-entrypoint.sh
#!/bin/bash

set -e

PARAMETER_STORE_PREFIX=${PARAMETER_STORE_PREFIX:-}

if [ -n "$PARAMETER_STORE_PREFIX" ]; then
  export SECRET_KEY_BASE=$(aws ssm get-parameters --name ${PARAMETER_STORE_PREFIX}.secret.key.base --with-decryption --query "Parameters[0].Value" --output text)
fi

exec "$@"
```


## デプロイ

ecs-deployを使ってる。pipを使うのが簡単に導入できる。

```
pip ecs-deploy
```

### Railsの場合のデプロイスクリプト例

このデプロイスクリプトをRailsアプリケーションを置いてるディレクトリで実行することでこの手順を自動化してる。

1. ECRにログイン
1. コンテナのイメージをビルド
1. ECRにビルドしたイメージをプッシュ
1. RailsでmigrationするECSタスクを実行
1. ECSサービスを更新

```:deploy.sh
#!/bin/sh

set -ex

account_id=xxxxxxxxxxxx
env=$1

eval $(aws ecr get-login --no-include-email --region ap-northeast-1)
docker build . -t $account_id.dkr.ecr.ap-northeast-1.amazonaws.com/kptboard-$env-app-server
docker push $account_id.dkr.ecr.ap-northeast-1.amazonaws.com/kptboard-$env-app-server:latest
aws ecs run-task --region ap-northeast-1 --cluster kptboard-$env --task-definition kptboard-$env-app-server-migration
ecs-deploy --cluster kptboard-$env --service-name kptboard-$env-app-server --image $account_id.dkr.ecr.ap-northeast-1.amazonaws.com/kptboard-$env-app-server:latest
```

```:bash
./deploy.sh prod
```