ENI制限の強化
2019年6月6日にAWSからawsvpcTrunkingアカウント設定が発表されました!
Amazon ECS が、awsvpc ネットワークモードのタスクに対する Elastic Network Interface (ENI) 制限の強化をサポート開始
今までインスタンスタイプごとに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が立ち上がる方が時間がかかるためもしかしたら間に合わずに詰まってしまうかもしれません。
あらかじめ待機させておいたり、Cloudwatch AlarmをトリガーにLambdaで一気に増やすなど色々な回避策を講じてきました。
便利なマネジメントツールが多くあるので合わせ技で回避することができる反面、管理すべきサービスが増えてしまうなんて状況です。
今回の追加によって、そういった煩わしさを少しだけ解消してくれるのではないかと期待しています。
awsvpcTrunkingアカウント設定をしてみる
では早速やってみましょう。
アカウント設定に従って進めていきます。
下準備
適当な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についてはこちらから確認してください。
Terraformでぱぱっと作ります。
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"]
}
}
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
}
まずは確認
こちらを参考にまずは設定を確認してみました。(当然disableなのですが…。)
$ 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"
}
]
}
無効になっていることを確認しました。
有効化
こちらを参考に有効化していきます。
今回はコンソールではなくCLIで有効化してみます。
$ 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"
}
]
}
問題なさそうですね。
実際に増やしてみる
では本当にENIの上限が変化するかを確認してみましょう。
今回はm5.large
のインスタンスを利用しているので、ホスト用を抜いた限界値9つまで上げてみます。
$ 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"
}
}
}
]
これで完了!と思いきや
$ 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 トランキングに関する考慮事項
awsvpcTrunking のオプトイン後に起動された新しい Amazon EC2 インスタンスのみが、
引き上げられた ENI 制限とトランクネットワークインターフェイスを受け取ります。
以前に起動されたインスタンスは、実行されたアクションに関係なく、これらの機能を受け取りません。
というわけでインスタンスを新しく立ち上げて確認します。
新しいエラーがなにやら発生しています。
$ 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
$
起動には成功しているみたいですね。
よくよく読むとここに書いてありました。
アカウント、IAM ユーザー、またはロールは、awsvpcTrunking アカウント設定をオプトインする必要があります。詳細については、「アカウント設定」を参照してください。
どうやら自分のIAMに対してawsvpcTrunkingアカウント設定を有効化するだけではダメみたいですね。
サービスのロールに設定しようと試みましたが、怒られてしまいました。
$ 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も最新のもので起動されています。
$ aws --region us-east-1 ec2 describe-instances --instance-ids i-012345abcdef67gh8 --query 'Reservations[].Instances[].ImageId[]'
[
"ami-02507631a9f7bc956"
]
rootからアカウントに対して許可が必要なんですかね?
近いうちにrootで作業できる環境で再度検証してみようと思います。
次回へつづく
おおーめっちゃ増えてるーってところを見たかったのですが残念
今週中にはrootで試してみようと思います。