サクっとAmazon ECSのクラスタを構築するためのterraformの設定

Amazon ECSはコンテナの管理をしてくれるサービスという体になってますが、実際には普通のEC2インスタンスの上で動作します。
コンピューティングリソースは、基本的にEC2のAuto Scaling Groupで管理をします。
別にAMIから手動で立てても動作しますが、落ちた時の再起動とかリソースのサイズを管理する上ではASの方が便利でしょう。

一応、CloudFormationのスニペットみたいなのはあったりするんですが、イマイチCloudFormationが好きではないのでECSのクラスタを構築してterraformで管理するための設定を準備しました。
参考までにまとめておきます。

まず、起動設定を管理できるterraformモジュールを作る。

```
variable "name" {}
variable "image_id" {
    default = "ami-010ed160"
}

variable "ecsInstanceRole" {}

variable "instance_type" {
    default = "t2.small"
}

variable "key_name" {}
variable "security_groups" {
    type = "list"
    default = ["sg-0000000"]
}

data "template_file" "ecs_config" {
    template = "${file("${path.module}/init.sh.tpl")}"
}

resource "aws_launch_configuration" "ecs" {
    name_prefix = "ecs-${var.name}-"
    image_id = "${var.image_id}"
    instance_type = "${var.instance_type}"
    iam_instance_profile = "${var.ecsInstanceRole}"
    key_name = "${var.key_name}"
    security_groups = "${var.security_groups}"
    user_data = "${data.template_file.ecs_config.rendered}"

    lifecycle {
      create_before_destroy = true
    }
}

output name {
  value = "${aws_launch_configuration.ecs.name}"
}
```

説明のために1ファイルにまとめて書いてますが、適宜分割してください。
変数の内容も、各々の環境次第では、default値を設定できたり不要なものがあったりすると思います。
AMIのIDは、ap-northeast-1用のECS-optimized Amazon Linuxの現時点(2016/10/26)での最新版です。
ちなみに、今のterraformだと変数化しづらいのでblock_deviceの設定をカスタムしたい場合等は別途カスタムAMIを作って管理した方が良い気がしています。

重要なのは、template_fileを使って、その結果をuser_dataに突っ込むことです。
ASでノードが起動した時の初期設定投入のためのスクリプトとして利用します。
内容は以下の様な感じになります。

```bash
#!/bin/bash

# Install JQ JSON parser
yum install -y jq aws-cli

# Get the current region and instance_id from the instance metadata
region=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/document | jq -r .region)
instance_id=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)

# Install the SSM agent RPM
yum install -y https://amazon-ssm-$region.s3.amazonaws.com/latest/linux_amd64/amazon-ssm-agent.rpm

# Tuning kernel parameter
sysctl -w net.ipv4.ip_local_port_range="10000 65000"
sysctl -w net.core.somaxconn=65535

# Fetch ECS cluster name from tag
cluster_name=$(aws ec2 describe-instances --region $region --instance-ids $instance_id | jq '.Reservations[0].Instances[0].Tags | from_entries | .ClusterName' -r)

echo ECS_CLUSTER=$cluster_name >> /etc/ecs/ecs.config
echo ECS_ENGINE_AUTH_TYPE=dockercfg >> /etc/ecs/ecs.config
echo 'ECS_ENGINE_AUTH_DATA={dummy dummy dummy}' >> /etc/ecs/ecs.config
echo ECS_AVAILABLE_LOGGING_DRIVERS=[\"json-file\",\"syslog\",\"fluentd\",\"awslogs\"] >> /etc/ecs/ecs.config
echo ECS_UPDATES_ENABLED=true >> /etc/ecs/ecs.config
echo ECS_ENGINE_TASK_CLEANUP_WAIT_DURATION=1h >> /etc/ecs/ecs.config
echo ECS_CONTAINER_STOP_TIMEOUT=5m >> /etc/ecs/ecs.config

mkdir /root/.docker
echo '{dummy dummy dummy}' > /root/.docker/config.json
```

ポイントはamazon-ssm-agentを入れておくことと、EC2のタグ名からECSのクラスタ名を取得してコンフィグに利用できるようにしておくことです。
amazon-ssm-agentがあれば、EC2の管理コンソールから複数ノードに対してまとめてコマンドが実行できるようになります。
後は、DockerのAUTH情報等を入れておくことや、ログドライバーの設定は割と必須だと思います。

続いて、Auto Scale Groupのterraformの設定を用意する。

```
resource "aws_ecs_cluster" "development-cluster" {
  name = "development-cluster"
}

# さっき作ったmodule
module "ecs_launch_config" {
    source = "../modules/ecs_launch_config"

    name = "development-cluster"
    ecsInstanceRole = "ecsInstanceRoleDev"
    key_name = "ecs-key"
}

resource "aws_autoscaling_group" "development-cluster" {
  name = "development-cluster"
  vpc_zone_identifier = ["subnet-00000000", "subnet-00000001"]
  max_size = 30
  min_size = 0
  health_check_grace_period = 300
  health_check_type = "EC2"
  launch_configuration = "${module.ecs_launch_config.name}"
  enabled_metrics = [
    "GroupMinSize",
    "GroupMaxSize",
    "GroupDesiredCapacity",
    "GroupInServiceInstances",
    "GroupPendingInstances",
    "GroupStandbyInstances",
    "GroupTerminatingInstances",
    "GroupTotalInstances"
  ]

  tag {
    key = "Name"
    value = "development-cluster-node"
    propagate_at_launch = true
  }

  tag {
    key = "ClusterName"
    value = "${aws_ecs_cluster.development-cluster.name}"
    propagate_at_launch = true
  }
}
```

とりあえず一緒に書きましたが、Auto Scaling Groupも分けてmodule化しても良いかもしれません。
enabled_metricsとか邪魔だし、vpc_zone_identifierもそうそう変わらん気がする。

起動設定は、基本的にはインスタンスに紐付くRoleかインスタンスタイプが共通である範囲では使い回せます。
今は、開発者向けステージング、全体向けステージング、本番でRoleをざっくり分けていて、本番環境はインスタンスタイプのスペックを二種類用意してクラスタ毎に使い分けてます。

一応、AMIが更新された時にもそこだけ書き換えれば、次回から起動するものはいい感じに更新された状態で起動するような設定になっていますが、実際に運用中のノードを更新していくのは割と面倒です。
開発環境とかだったら別に上にコンテナが乗ってようが、もろともにシャットダウンすれば勝手に復活して更新済みノードで戻ってきてくれるんですが、production環境だと落とすとまずいコンテナがあったりするんで、一時的にコンテナ稼動数を増やして、古いノードからコンテナを落とすって感じにことをしてローリングアップデート的なことをやる必要がある場合もあります。