EKSマネージドワーカーに任意のセキュリティグループをつける

EKSのマネージドワーカーはとても便利ですがデプロイされるEC2につくセキュリティグループはEKSが作成したものです。自分で作成した任意のセキュリティグループを付与するにはEC2の起動テンプレートを作成し、その中でセキュリティグループを指定します。

これをTerraformで設定する方法を残しておきます。

# マネージドノードグループが作成する起動テンプレートの確認

Terraformで作成する起動テンプレートの参考にするためまずは普通にマネージドワーカーをデプロイして設定を確認します。

マネージメントコンソールなどからマネージドノードグループが作成する起動テンプレートのIDを確認します。たとえばマネージメントコンソールからだとEC2->インタンス->テンプレートの起動に起動テンプレートはあります。またはEC2->Auto ScalingグループでEKSマネージドノードグループのオートスケーリンググループから確認しても良いです。

マネージメントコンソールからでも起動テンプレートの設定を確認できるが念の為コマンドで設定を確認します。

``` sh
$ aws ec2 describe-launch-template-versions --launch-template-id lt-084a093919e4b2c36
```

出力例

```json
{
    "LaunchTemplateVersions": [
        {
            "LaunchTemplateId": "lt-084a093919e4b2c36",
            "LaunchTemplateName": "eks-08bd9e75-1ab5-77ea-3e5e-96f926cdda77",
            "VersionNumber": 1,
            "CreateTime": "2021-08-13T00:30:03+00:00",
            "CreatedBy": "arn:aws:sts::461548955069:assumed-role/AWSServiceRoleForAmazonEKSNodegroup/EKS",
            "DefaultVersion": true,
            "LaunchTemplateData": {
                "IamInstanceProfile": {
                    "Name": "eks-08bd9e75-1ab5-77ea-3e5e-96f926cdda77"
                },
                "BlockDeviceMappings": [
                    {
                        "DeviceName": "/dev/xvda",
                        "Ebs": {
                            "DeleteOnTermination": true,
                            "VolumeSize": 30,
                            "VolumeType": "gp2"
                        }
                    }
                ],
                "NetworkInterfaces": [
                    {
                        "DeviceIndex": 0,
                        "Groups": [
                            "sg-0bd04e1bd3c43d395",
                            "sg-0195e42ca26f31d49"
                        ]
                    }
                ],
                "ImageId": "ami-02a49655b336f1b47",
                "InstanceType": "t3.medium",
                "KeyName": "hoge",
                "UserData": "TUlNRS1WZXJzaW...,
                "MetadataOptions": {
                    "HttpPutResponseHopLimit": 2
                }
            }
        }
    ]
}
```

userdataをbase64デコードすると以下の内容

```sh
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="//"

--//
Content-Type: text/x-shellscript; charset="us-ascii"
#!/bin/bash
set -ex
B64_CLUSTER_CA=LS0tLS1CRUdJTiBDRVJUSUZ...
API_SERVER_URL=https://B575989085247853299602E94FEED13C.gr7.ap-northeast-1.eks.amazonaws.com
K8S_CLUSTER_DNS_IP=172.20.0.10
/etc/eks/bootstrap.sh o2-dev-cluster --kubelet-extra-args '--node-labels=eks.amazonaws.com/nodegroup-image=ami-02a49655b336f1b47,eks.amazonaws.com/capacityType=ON_DEMAND,eks.amazonaws.com/nodegroup=o2-dev-node-group' --b64-cluster-ca $B64_CLUSTER_CA --apiserver-endpoint $API_SERVER_URL --dns-cluster-ip $K8S_CLUSTER_DNS_IP

--//--
```

上記結果を参考にTerraformを作成します。

# Terraform

Terraformは以下5種類とvariablesを作成します。

- iam.tf
- managed-node-group.tf
- launch-config.tf
- sg.tf
- eks.tf
- variables.tf

## iam.tf

EKSおよびマネージドノードグループに必要なIAMを作成します。元ネタはAWSドキュメントにある[Amazon EKS クラスター の IAM ロール](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/service_IAM_role.html)、[Amazon EKS ノードの IAM ロール](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/create-node-role.html)を参考にしています。

```iam.tf
# eks cluster
resource "aws_iam_role" "eks_cluster" {
  name = "${var.base_name}-eks-cluster-role"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "eks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY

  tags = {
    "Name" = "${var.base_name}-eks-cluster-role"
  }
}

resource "aws_iam_role_policy_attachment" "eks_cluster" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
  role       = aws_iam_role.eks_cluster.name
}

resource "aws_iam_role_policy_attachment" "eks_vpc_resource_controller" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSVPCResourceController"
  role       = aws_iam_role.eks_cluster.name
}

# managed node group
resource "aws_iam_role" "eks_node_group" {
  name = "${var.base_name}-eks-node-group"

  assume_role_policy = jsonencode({
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "ec2.amazonaws.com"
      }
    }]
    Version = "2012-10-17"
  })

  tags = {
    "Name" = "${var.base_name}-eks-node-group"
  }
}

resource "aws_iam_role_policy_attachment" "eks_worker_node_policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
  role       = aws_iam_role.eks_node_group.name
}

resource "aws_iam_role_policy_attachment" "eks_cni_policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
  role       = aws_iam_role.eks_node_group.name
}

resource "aws_iam_role_policy_attachment" "ec2_ecr_ro" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
  role       = aws_iam_role.eks_node_group.name
}
```

## managed-node-group.tf

マネージドノードグループの設定をします。ここで任意のセキュリティグループを付けたいため、`launch_template`を指定します。また、`eks-node-group.tf`と`launch-config.tf`にはいつくか両方ともで設定できるパラメータがあります。そのようなパラメータを両方で設定するとエラーになるため一方でのみ設定します。どちらで設定すべきかはAWSのドキュメント[起動テンプレートのサポート](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/launch-templates.html)に案内があります。

```managed-node-group.tf
resource "aws_eks_node_group" "worker" {
  cluster_name    = var.eks_name
  node_group_name = "${var.base_name}-node-group"
  node_role_arn   = aws_iam_role.eks_node_group.arn
  subnet_ids      = var.public_subnet_ids

  launch_template {
    id      = aws_launch_template.eks_node_group.id
    version = aws_launch_template.eks_node_group.latest_version
  }

  scaling_config {
    desired_size = var.eks-node_desired_capacity
    max_size     = var.eks-node_max_size
    min_size     = var.eks-node_min_size
  }

  tags = {
    "Name" = "${var.base_name}-node-group"
  }

  lifecycle {
    ignore_changes = [scaling_config[0].desired_size]
  }

  depends_on = [
    aws_eks_cluster.this
  ]
}
```

## launch-config.tf

起動テンプレートの設定をします。基本的には[マネージドノードグループが作成する起動テンプレートの確認](#マネージドノードグループが作成する起動テンプレートの確認で確認した内容をそのままTerraformに落とし込んでいます。

`network_interfaces`に目的である任意のセキュリティグループを指定します。また、EKSマスターと通信するためのセキュリティグループも必要です。（元ネタ：[カスタムセキュリティグループを使用する](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/launch-templates.html#launch-template-security-groups)）

USERDATAの中で`K8S_CLUSTER_DNS_IP`を指定しています。これはCoreDNSのServiceのIPアドレスですが、ServiceネットワークのCIDRによってIPアドレスが異なります。デフォルトのServiceネットワークのCIDRはTerraformの`aws_eks_cluster`リソースのドキュメント[kubernetes_network_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_cluster#kubernetes_network_config)にある通り、EKSをデプロイするVPCのCIDRとは異なるCIDR（10.100.0.0/16 or 172.20.0.0/16）を使用します。今回はEKSの設定で`service_ipv4_cidr`を指定し、そのvaluesを加工してCoreDNSのServiceのIPアドレスを指定しています。

```launch-config.tf
locals {
  eks-nodes_init_script = <<USERDATA
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="//"

--//
Content-Type: text/x-shellscript; charset="us-ascii"
#!/bin/bash
set -ex
B64_CLUSTER_CA=${aws_eks_cluster.this.certificate_authority.0.data}
API_SERVER_URL=${aws_eks_cluster.this.endpoint}
K8S_CLUSTER_DNS_IP=${replace(var.eks_service_ipv4_cidr, "0/16", "10")}
/etc/eks/bootstrap.sh ${var.eks_name} --kubelet-extra-args '--node-labels=eks.amazonaws.com/nodegroup-image=${data.aws_ami.eks-node.id},eks.amazonaws.com/capacityType=ON_DEMAND,eks.amazonaws.com/nodegroup=${var.base_name}-node-group' --b64-cluster-ca $B64_CLUSTER_CA --apiserver-endpoint $API_SERVER_URL --dns-cluster-ip $K8S_CLUSTER_DNS_IP

--//--
USERDATA
}

data "aws_ami" "eks-node" {
  most_recent = true
  owners      = ["602401143452"]

  filter {
    name   = "name"
    values = ["amazon-eks-node-${var.eks_version}-*"]
  }
}

resource "aws_launch_template" "eks_node_group" {
  name = "${var.base_name}-eks_node_group"

  block_device_mappings {
    device_name = "/dev/xvda"

    ebs {
      delete_on_termination = true
      volume_size           = var.eks-node_volume_size
      volume_type           = "gp2"
    }
  }

  network_interfaces {
    device_index    = 0
    security_groups = [aws_security_group.eks-node.id, aws_security_group.eks.id]
  }

  image_id      = data.aws_ami.eks-node.id
  instance_type = var.eks-node_instance_type
  key_name      = var.eks-node_key_name
  user_data     = base64encode(local.eks-nodes_init_script)

  depends_on = [
    aws_eks_cluster.this
  ]

  tags = {
    "Name" = "${var.base_name}-eks_node_group"
  }
}
```

## sg.tf

セキュリティグループを設定します。[カスタムセキュリティグループを使用する](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/launch-templates.html#launch-template-security-groups)にある通り、マネージドノードに付けたいセキュリティグループとEKSマスターと通信するためのセキュリティグループを作ります。

セキュリティグループのルールは[Amazon EKS セキュリティグループの考慮事項](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/sec-group-reqs.html)を参考にします。

```sg.tf
# eks cluster
resource "aws_security_group" "eks" {
  name        = "${var.base_name}-eks-sg"
  description = "for eks sg"
  vpc_id      = var.vpc_id

  tags = {
    "Name" = "${var.base_name}-eks-sg",
  }
}

resource "aws_security_group_rule" "eks_in_443_self" {
  type              = "ingress"
  from_port         = 443
  to_port           = 443
  protocol          = "tcp"
  self              = true
  security_group_id = aws_security_group.eks.id
}

resource "aws_security_group_rule" "eks_in_1025-65535_self" {
  type              = "ingress"
  from_port         = 1025
  to_port           = 65535
  protocol          = "tcp"
  self              = true
  security_group_id = aws_security_group.eks.id
}

resource "aws_security_group_rule" "eks_out_all" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  ipv6_cidr_blocks  = ["::/0"]
  security_group_id = aws_security_group.eks.id
}

# managed node group
resource "aws_security_group" "eks-node" {
  name        = "${var.base_name}-eks-node-sg"
  description = "for eks-node server sg"
  vpc_id      = var.vpc_id

  tags = {
    "Name"                                  = "${var.base_name}-eks-node-sg",
    "kubernetes.io/cluster/${var.eks_name}" = "owned"
  }
}

resource "aws_security_group_rule" "eks-node_in_30000-65535" {
  type              = "ingress"
  from_port         = 30000
  to_port           = 65535
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.eks-node.id
}

resource "aws_security_group_rule" "eks-node_in_self" {
  type              = "ingress"
  from_port         = 0
  to_port           = 0
  protocol          = -1
  self              = true
  security_group_id = aws_security_group.eks-node.id
}

resource "aws_security_group_rule" "eks-node_out_all" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  ipv6_cidr_blocks  = ["::/0"]
  security_group_id = aws_security_group.eks-node.id
}
```

## eks.tf

EKSの設定です。マネージドワーカーと通信するため、`security_group_ids`に作成したEKSクラスタ用のセキュリティグループを指定します。また、[launch-configtf](#launch-configtf)のUSERDATAでCoreDNSのIPアドレスを指定したいため、`kubernetes_network_config`も設定します。

```eks.tf
resource "aws_eks_cluster" "this" {
  name     = var.eks_name
  role_arn = aws_iam_role.eks_cluster.arn
  version  = var.eks_version

  vpc_config {
    endpoint_private_access = var.eks_endpoint_private_access
    endpoint_public_access  = var.eks_endpoint_public_access
    public_access_cidrs     = var.eks_public_access_cidrs
    security_group_ids      = [aws_security_group.eks.id]
    subnet_ids              = flatten([var.private_subnet_ids, var.public_subnet_ids])
  }

  enabled_cluster_log_types = var.eks_enabled_cluster_log_types

  kubernetes_network_config {
    service_ipv4_cidr = var.eks_service_ipv4_cidr
  }

  tags = {
    "Name" = var.eks_name
  }

  depends_on = [
    aws_iam_role_policy_attachment.eks_cluster,
    aws_iam_role_policy_attachment.eks_vpc_resource_controller,
  ]
}
```

## variables.tf

```variables.tf
variable "base_name" {
  description = "作成するリソースに付与する接頭語"
  type        = string
}

## EKS
variable "eks_version" {
  description = "EKSのバージョン"
  type        = string
}

variable "eks_name" {
  description = "EKSクラスタの名前"
  type        = string
}

variable "eks_endpoint_private_access" {
  description = "VPC内からのアクセスを許可するか"
  type        = bool
}

variable "eks_endpoint_public_access" {
  description = "VPC外からのアクセスを許可するか"
  type        = bool
}

variable "eks_public_access_cidrs" {
  description = "VPC外からのアクセスを許可するCIDR"
  type        = list(string)
}

variable "eks_enabled_cluster_log_types" {
  description = "保存するマスターコンポーネントのログ"
  type        = list(string)
}

variable "eks_service_ipv4_cidr" {
  description = "serviceのCIDR"
  type        = string
}

## EKS node group
variable "eks-node_desired_capacity" {
  description = "EKSノードグループの希望数"
  type        = number
}

variable "eks-node_max_size" {
  description = "EKSノードグループの最大数"
  type        = number
}

variable "eks-node_min_size" {
  description = "EKSノードグループの最小数"
  type        = number
}

variable "eks-node_volume_size" {
  description = "EKSノードのディスク容量"
  type        = number
}

variable "eks-node_instance_type" {
  description = "EKSノードのインスタンスタイプ"
  type        = string
}

variable "eks-node_key_name" {
  description = "EKSノードのキーペア名"
  type        = string
}
```

## tfvarsの例

```terraform
# common parameter
base_name = "test-eks"

# network parameter
private_subnet_ids = [
  "subnet-04e80b9b64g8ab944",
  "subnet-05dec3759d2ff895a",
]
public_subnet_ids = [
  "subnet-0fd24b7b1d9f11078",
  "subnet-0e12b21b7fc7ccf77",
]
vpc_cidr = "10.1.0.0/16"
vpc_id   = "vpc-0b02175fcb37cb133"

# eks cluster
eks_version                   = "1.21"
eks_name                      = "test-eks-cluster"
eks_endpoint_private_access   = true
eks_endpoint_public_access    = true
eks_public_access_cidrs       = ["126.72.69.141/32"]
eks_enabled_cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
eks_service_ipv4_cidr         = "172.20.0.0/16"

# eks node group
eks-node_desired_capacity = 4
eks-node_max_size         = 4
eks-node_min_size         = 4
eks-node_volume_size      = 30
eks-node_instance_type    = "m5.large"
eks-node_key_name         = "hoge"
```

# あとがき

オートスケーリンググループが使用する起動テンプレートは上記**自分で作成した起動テンプレート**ではなく、自分で作成したテンプレートをコピーして新たに作られる起動テンプレートが使用されるようです。マネージドノードグループの設定でテンプレートのバージョンを`latest`で指定していれば自分で作成したテンプレートを更新するとコピーの起動テンプレートも更新されました。

ただ、ここまでいろいろやるならセルフマネージドのオートスケーリンググループ作るのとあんまり変わらない気もした。