awsvpcTrunking設定を試す【ECS】

# ENI制限の強化
2019年6月6日にAWSからawsvpcTrunkingアカウント設定が発表されました！
[Amazon ECS が、awsvpc ネットワークモードのタスクに対する Elastic Network Interface (ENI) 制限の強化をサポート開始](https://aws.amazon.com/jp/about-aws/whats-new/2019/06/Amazon-ECS-Improves-ENI-Density-Limits-for-awsvpc-Networking-Mode/)
今までインスタンスタイプごとにENIの上限は変更できませんでしたが、この度変更することができるようになりました。
利用可能な条件については下記です。

 - ECS が使用できるすべてのリージョン
 - Amazon ECS最適化AMI

 *現時点では、Windows コンテナはサポートされていません。

awsvpcモードを利用したEC2起動タイプのコンテナが更に使い勝手がよくなりましたので、実際にやってみようと思います。

# これまでのECS運用
私がECSを利用する際、以下の2つが選択において考慮するポイントでした。

 - タスクの増減が頻繁か
 - awsvpcモードを利用するか

タスクの増減が頻繁に起こるようなコンテナですと、EC2起動タイプよりもFargateの方がスムーズに増減が実現できかつスケーリングの設定などの煩わしさから解放されるためできれば利用したいです。
またawsvpcモードを利用する場合はインスタンス側のENI制限に依存するためFargateを選択して制限から解放されたいので、こちらも可能な限りFargateを利用したいと思っています。

しかし、今年の1月に安くなったとはいえ金額的にFargateの方が高いため躊躇うこともあると思います。
そのためEC2起動タイプを選択する方はまだまだ少なくないと思いますが、その際にぶつかるのが上述したENI問題です。
awsvpcモードの場合、ENIをコンテナにアタッチして利用するため以下の利点が見込めます。

- ALB / NLBにIPターゲットとして登録ができる
- タスクごとにSecurityGroupを紐付けることができる
- 同一ホスト内でのタスクポートマッピングを考慮する必要がない

非常な便利かつ安価に使える反面、何度も繰り返しますがENIの数がインスタンスに依存します。
`m5.large`のインスタンスの場合ENIは3つが上限です。
無料利用枠で750h利用できる`t2.micro`に至っては2つが上限となっております。

例えば`m5.large`のインスタンスでECS Clusterを利用した場合、
まずはホストがENIを1つ利用するため1インスタンスにつき最大2つまでawsvpcモードを利用したコンテナが利用できます。
負荷が集中しコンテナが`2 -> 10`までスケールアウトが必要になった際、`m5.large`インスタンスは5つ必要になります。
当然コンテナよりもEC2が立ち上がる方が時間がかかるためもしかしたら間に合わずに詰まってしまうかもしれません。

＊こんなエラーを見たことがある方もいるかと思います。
<img width="815" alt="trunk_01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/b63bc6bc-5a4e-ef72-36fd-913834546216.png">


あらかじめ待機させておいたり、Cloudwatch AlarmをトリガーにLambdaで一気に増やすなど色々な回避策を講じてきました。
便利なマネジメントツールが多くあるので合わせ技で回避することができる反面、管理すべきサービスが増えてしまうなんて状況です。

今回の追加によって、そういった煩わしさを少しだけ解消してくれるのではないかと期待しています。

# awsvpcTrunkingアカウント設定をしてみる
では早速やってみましょう。
[アカウント設定](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-account-settings.html)に従って進めていきます。

### 下準備
適当なECS周りのものを用意します。
今回は以下のものを用意しました。

 - ECS Cluster
 - Cluster用Launch Config 
 - Cluster用Instance Profile
 - Cluster用IAM Role
 - Cluster用IAM Policy
 - Cluster用Security Group
 - ECS Service
 - Service用IAM Role
 - Service用IAM Policy
 - Service用Security Group
 - ECS Task definition
 - Task用IAM Role
 - Task用IAM Policy

ECS最適化AMIについては[こちら](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-optimized_AMI_launch_latest.html)から確認してください。

<details><summary>Terraformでぱぱっと作ります。</summary><div>

```go:trunking_ecs.tf
resource "aws_ecs_cluster" "trunking-cluster" {
  name = "trunking-ecs"
}

data "template_file" "trunking-ecs-userdata" {
  template = "${file("userdata-template/trunking-ecs.tpl")
}

resource "aws_launch_configuration" "trunking-ecs" {
  name_prefix          = "trunking-ecs-launch-config-"
  image_id             = "ami-02507631a9f7bc956"
  instance_type        = "m5.large"
  iam_instance_profile = "${aws_iam_instance_profile.trunking-ecs.name}"
  key_name             = "${var.own_key}"
  user_data            = "${data.template_file.trunking-ecs-userdata.rendered}"
  security_groups      = ["${aws_security_group.trunking-ecs.id}"]

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_iam_role" "trunking-ecs-role" {
  name               = "${var.region}-trunking-ecs"
  assume_role_policy = "${file("./policy/iam_assumerole.json")}"
}

resource "aws_iam_instance_profile" "trunking-ecs" {
  depends_on = ["aws_iam_role.trunking-ecs-role"]
  name       = "${var.region}-trunking-ecs"
  role       = "${aws_iam_role.trunking-ecs-role.name}"
}

resource "aws_iam_role_policy_attachment" "trunking-ecs-attach" {
  depends_on = ["aws_iam_role.trunking-ecs-role"]
  role       = "${aws_iam_role.trunking-ecs-role.name}"
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
}

resource "aws_security_group" "trunking-ecs" {
  name        = "trunking-ecs"
  description = "trunking ECS security Group"
  vpc_id      = "${aws_vpc.test-vpc.id}"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["${aws_vpc.test-vpc.cidr_block}"]
  }

  ingress {
    from_port   = 31000
    to_port     = 61000
    protocol    = "tcp"
    cidr_blocks = ["${aws_vpc.test-vpc.cidr_block}"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags {
    "Name"    = "trunking-ecs-ingress"
    "Service" = "trunking"
  }
}

resource "aws_autoscaling_group" "trunking-ecs" {
  name                      = "trunking-ecs-asg"
  max_size                  = 2
  min_size                  = 1
  health_check_grace_period = 300
  health_check_type         = "ELB"
  desired_capacity          = 1
  force_delete              = true
  launch_configuration      = "${aws_launch_configuration.trunking-ecs.name}"
  vpc_zone_identifier       = ["${aws_subnet.private.*.id}"]

  tag {
    key                 = "Name"
    value               = "${var.region}-trunking-ecs"
    propagate_at_launch = true
  }

  tag {
    key                 = "Service"
    value               = "trunking"
    propagate_at_launch = true
  }

  lifecycle {
    create_before_destroy = true
    ignore_changes        = ["desired_capacity"]
  }
}
```


```go:trunking_service.tf
resource "aws_ecs_service" "trunking-service" {
  name            = "trunking"
  cluster         = "${aws_ecs_cluster.trunking-cluster.id}"
  task_definition = "${aws_ecs_task_definition.trunking-task.arn}"
  desired_count   = 2

  network_configuration {
    security_groups = ["${aws_security_group.trunking-service.id}"]
    subnets         = ["${aws_subnet.private.*.id}"]
  }

  load_balancer {
    target_group_arn = "${aws_lb_target_group.trunking.arn}"
    container_name   = "trunking-service"
    container_port   = "24224"
  }

  ordered_placement_strategy {
    type  = "spread"
    field = "attribute:ecs.availability-zone"
  }

  ordered_placement_strategy {
    type  = "spread"
    field = "instanceId"
  }

  lifecycle {
    ignore_changes = ["desired_count"]
  }
}

resource "aws_security_group" "trunking-service" {
  name        = "trunking-service"
  description = "trunking service security Group"
  vpc_id      = "${aws_vpc.test-vpc.id}"

  ingress {
    from_port   = 24224
    to_port     = 24224
    protocol    = "tcp"
    self        = true
    cidr_blocks = ["${aws_vpc.test-vpc.cidr_block}"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags {
    "Name"    = "trunking-service-ingress"
    "Service" = "trunking"
  }
}

data "template_file" "trunking-task" {
  template = "${file("task-definitions/trunking-task.tpl")}"
}

resource "aws_ecs_task_definition" "trunking-task" {
  family                = "trunking"
  network_mode          = "awsvpc"
  container_definitions = "${data.template_file.trunking-task.rendered}"
  task_role_arn         = "${aws_iam_role.task-execution.arn}"

  tags {
    Name    = "trunking-task"
    Service = "trunking"
  }
}

resource "aws_iam_role" "trunking-service-role" {
  name               = "${var.region}-trunking-service"
  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

resource "aws_iam_role_policy_attachment" "trunking-service-attach" {
  role       = "${aws_iam_role.trunking-service-role.name}"
  policy_arn = "${aws_iam_policy.trunking-cluster.arn}"
}

resource "aws_cloudwatch_log_group" "trunking" {
  name = "/${var.service_name}/trunking"
}

resource "aws_iam_policy" "trunking-cluster" {
  count = "${aws_ecs_cluster.trunking-cluster.count}"
  name  = "${var.region}-TrunkingServicePolicy"

  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ECSTaskTrunking",
            "Effect": "Allow",
            "Action": [
                "ec2:AttachNetworkInterface",
                "ec2:CreateNetworkInterface",
                "ec2:CreateNetworkInterfacePermission",
                "ec2:DeleteNetworkInterface",
                "ec2:DeleteNetworkInterfacePermission",
                "ec2:Describe*",
                "ec2:DetachNetworkInterface",
                "elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
                "elasticloadbalancing:DeregisterTargets",
                "elasticloadbalancing:Describe*",
                "elasticloadbalancing:RegisterInstancesWithLoadBalancer",
                "elasticloadbalancing:RegisterTargets",
                "route53:ChangeResourceRecordSets",
                "route53:CreateHealthCheck",
                "route53:DeleteHealthCheck",
                "route53:Get*",
                "route53:List*",
                "route53:UpdateHealthCheck",
                "servicediscovery:DeregisterInstance",
                "servicediscovery:Get*",
                "servicediscovery:List*",
                "servicediscovery:RegisterInstance",
                "servicediscovery:UpdateInstanceCustomHealthStatus",
                "ecr:BatchCheckLayerAvailability",
                "ecr:BatchGetImage",
                "ecr:GetDownloadUrlForLayer",
                "ecr:GetAuthorizationToken",
                "application-autoscaling:*",
                "cloudwatch:DescribeAlarms",
                "cloudwatch:PutMetricAlarm"
            ],
            "Resource": "*"
        },
        {
            "Sid": "TrunkingECSTagging",
            "Effect": "Allow",
            "Action": [
                "ec2:CreateTags"
            ],
            "Resource": "arn:aws:ec2:*:*:network-interface/*"
        }
    ]
}
EOF
}
```

</div></details>

できました。
<img width="1163" alt="trunk_02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/2bdcf6b2-8b5d-3a0f-a74c-9402f6650bee.png">


### まずは確認
[こちら](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-viewing-longer-id-settings)を参考にまずは設定を確認してみました。（当然disableなのですが…。）

```bash:before_enable
$ aws --region us-east-1 ecs list-account-settings --effective-settings
{
    "settings": [
        {
            "name": "awsvpcTrunking",
            "value": "disabled",
            "principalArn": "arn:aws:iam::123456789012:user/hogehoge.fugafuga"
        },
        {
            "name": "containerInstanceLongArnFormat",
            "value": "enabled",
            "principalArn": "arn:aws:iam::123456789012:root"
        },
        {
            "name": "serviceLongArnFormat",
            "value": "enabled",
            "principalArn": "arn:aws:iam::123456789012:root"
        },
        {
            "name": "taskLongArnFormat",
            "value": "enabled",
            "principalArn": "arn:aws:iam::123456789012:root"
        }
    ]
}
```

コンソールからも
<img width="1419" alt="trunk_03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/c1bb4186-0c56-2ca8-f928-7952856bc241.png">


無効になっていることを確認しました。

### 有効化
[こちら](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-modifying-longer-id-settings)を参考に有効化していきます。
今回はコンソールではなくCLIで有効化してみます。

```bash:enable_trunking
$ aws ecs --region us-east-1 put-account-setting --name awsvpcTrunking --value enabled --principal-arn arn:aws:iam::123456789012:user/hogehoge.fugafuga
{
    "setting": {
        "name": "awsvpcTrunking",
        "value": "enabled",
        "principalArn": "arn:aws:iam::123456789012:user/hogehoge.fugafuga"
    }
}
$
$ aws --region us-east-1 ecs list-account-settings --principal-arn arn:aws:iam::123456789012:user/hogehoge.fugafuga --effective-settings
{
    "settings": [
        {
            "name": "awsvpcTrunking",
            "value": "enabled",
            "principalArn": "arn:aws:iam::123456789012:user/hogehoge.fugafuga"
        },
        {
            "name": "containerInstanceLongArnFormat",
            "value": "enabled",
            "principalArn": "arn:aws:iam::123456789012:root"
        },
        {
            "name": "serviceLongArnFormat",
            "value": "enabled",
            "principalArn": "arn:aws:iam::123456789012:root"
        },
        {
            "name": "taskLongArnFormat",
            "value": "enabled",
            "principalArn": "arn:aws:iam::123456789012:root"
        }
    ]
}
```

コンソールからも確認
<img width="1431" alt="trunk_04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/ea324ff6-3ac2-8f31-2f83-fc43f03a7e70.png">


問題なさそうですね。

# 実際に増やしてみる
では本当にENIの上限が変化するかを確認してみましょう。
今回は`m5.large`のインスタンスを利用しているので、ホスト用を抜いた限界値9つまで上げてみます。

```bash:chage_desired_count
$ aws --region us-east-1 ecs update-service --cluster trunking-ecs --service trunking --desired-count 9 --query 'service[].desiredCount'
{
    "desiredCount": 9,
}
$
$ aws --region us-east-1 ecs describe-services --cluster trunking-ecs --services trunking --query 'services[].deployments[]'
[
    {
        "id": "ecs-svc/9223370475408119189",
        "status": "PRIMARY",
        "taskDefinition": "arn:aws:ecs:us-east-1:123456789012:task-definition/trunking:2",
        "desiredCount": 9,
        "pendingCount": 0,
        "runningCount": 2,
        "createdAt": 1561446656.618,
        "updatedAt": 1561457505.944,
        "launchType": "EC2",
        "networkConfiguration": {
            "awsvpcConfiguration": {
                "subnets": [
                    "subnet-12345678901234567",
                    "subnet-76543210987654321"
                ],
                "securityGroups": [
                    "sg-01928374656473829"
                ],
                "assignPublicIp": "DISABLED"
            }
        }
    }
]
```

これで完了！と思いきや

```bash:error
$ aws --region us-east-1 ecs describe-services --cluster trunking-ecs --services trunking --query 'services[].events[]' | head -n 6
[
    {
        "id": "8e4600d0-206f-4bed-9388-2a86b4bcb54c",
        "createdAt": 1561457461.911,
        "message": "(service trunking) was unable to place a task because no container instance met all of its requirements. The closest matching (container-instance dab5c255b71247dba74f5cb126eb6db7) encountered error \"RESOURCE:ENI\". For more information, see the Troubleshooting section of the Amazon ECS Developer Guide."
    },
```

どうやら即時反映ではなく設定後に起動されたインスタンスから反映されるようです。
[ENI トランキングに関する考慮事項](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/container-instance-eni.html#eni-trunking-considerations)
>awsvpcTrunking のオプトイン後に起動された新しい Amazon EC2 インスタンスのみが、
引き上げられた ENI 制限とトランクネットワークインターフェイスを受け取ります。
以前に起動されたインスタンスは、実行されたアクションに関係なく、これらの機能を受け取りません。

というわけでインスタンスを新しく立ち上げて確認します。
新しいエラーがなにやら発生しています。

```bash:error_2
$ aws --region us-east-1 ecs describe-services --cluster trunking-ecs --services trunking --query 'services[].events[]' | head -n 8
[
    {
        "id": "5a41e1a5-7c35-4795-a9f5-1636d2dab3c5",
        "createdAt": 1561458498.055,
        "message": "(service trunking) was unable to place a task because no container instance met all of its requirements. The closest matching (container-instance dab5c255b71247dba74f5cb126eb6db7) doesn't have the agent connected. For more information, see the Troubleshooting section of the Amazon ECS Developer Guide."
    },
```

インスタンスに入って`/var/log/ecs`のログを確認

```
$ cd /var/log/ecs
$ cat ecs-init.log
2019-06-25T10:40:45Z [INFO] pre-start
2019-06-25T10:40:46Z [INFO] start
2019-06-25T10:40:46Z [INFO] No existing agent container to remove.
2019-06-25T10:40:46Z [INFO] Starting Amazon Elastic Container Service Agent
$
$ cat ecs-agent.log.2019-06-25-10 | head
2019-06-25T10:40:47Z [INFO] Loading configuration
2019-06-25T10:40:47Z [INFO] Image excluded from cleanup: amazon/amazon-ecs-agent:latest
2019-06-25T10:40:47Z [INFO] Image excluded from cleanup: amazon/amazon-ecs-pause:0.1.0
2019-06-25T10:40:47Z [INFO] Amazon ECS agent Version: 1.29.0, Commit: a190a73f
2019-06-25T10:40:47Z [INFO] Creating root ecs cgroup: /ecs
2019-06-25T10:40:47Z [INFO] Creating cgroup /ecs
2019-06-25T10:40:47Z [INFO] Loading state! module="statemanager"
2019-06-25T10:40:47Z [INFO] Event stream ContainerChange start listening...
2019-06-25T10:40:48Z [INFO] Registering Instance with ECS
2019-06-25T10:40:48Z [INFO] Remaining mem: 7681
$ 
$ docker ps
CONTAINER ID        IMAGE                                                                        COMMAND                  CREATED             STATUS              PORTS               NAMES
2e9ce16d7b40        123456789012.dkr.ecr.us-east-1.amazonaws.com/test:trunking                   "/opt/sh/startup.sh …"   20 minutes ago      Up 20 minutes                           ecs-trunking-2-trunking-service-de8897e0cb8dcded5b00
d84fc735402b        123456789012.dkr.ecr.us-east-1.amazonaws.com/test:trunking                   "/opt/sh/startup.sh …"   20 minutes ago      Up 20 minutes                           ecs-trunking-2-trunking-service-d4918bbcbbf1fbd02d00
40177ea3b5d2        amazon/amazon-ecs-pause:0.1.0                                                "./pause"                20 minutes ago      Up 20 minutes                           ecs-trunking-2-internalecspause-c8f988dcbae8b9bd5500
952b16278748        amazon/amazon-ecs-pause:0.1.0                                                "./pause"                20 minutes ago      Up 20 minutes                           ecs-trunking-2-internalecspause-deabf4c4d4bc81eb8a01
f1413a19e94b        amazon/amazon-ecs-agent:latest                                               "/agent"                 20 minutes ago      Up 20 minutes                           ecs-agent
$ 
```
起動には成功しているみたいですね。
よくよく読むと[ここ](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/container-instance-eni.html#eni-trunking-launching)に書いてありました。
>アカウント、IAM ユーザー、またはロールは、awsvpcTrunking アカウント設定をオプトインする必要があります。詳細については、「アカウント設定」を参照してください。

どうやら自分のIAMに対してawsvpcTrunkingアカウント設定を有効化するだけではダメみたいですね。
サービスのロールに設定しようと試みましたが、怒られてしまいました。

```bash:enable_service_role
$ aws ecs --region us-east-1 put-account-setting --name awsvpcTrunking --value enabled --principal-arn arn:aws:iam::123456789012:role/us-east-1-trunking-service

An error occurred (InvalidParameterException) when calling the PutAccountSetting operation: Only the root user can view or modify the account settings for another user. You do not need to specify a principalArn value to change your own account settings.
$ 
```
AMIも最新のもので起動されています。

```bash:ami_confirm
$ aws --region us-east-1 ec2 describe-instances --instance-ids i-012345abcdef67gh8 --query 'Reservations[].Instances[].ImageId[]'
[
    "ami-02507631a9f7bc956"
]
```
**root**からアカウントに対して許可が必要なんですかね？
近いうちにrootで作業できる環境で再度検証してみようと思います。

# 次回へつづく
おおーめっちゃ増えてるーってところを見たかったのですが残念
今週中にはrootで試してみようと思います。