なになれ

IT系のことを記録していきます。

TerraformでEKSの環境を作ることで環境構築方法を統一したい

EKSの環境を作るにはeksctlという便利なツールがあります。
これを使えば、様々なパターンの環境構築に簡単に対応できて、とても便利です。
ただし、eksctlはEKS周辺の環境構築しか行うことができません。
今の環境ではAWSリソースの管理はTerraformで実施しているので、EKSに関係ないAWSリソースはTerraform管理になります。
このままだと、Terraformで管理するリソースとeksctlで管理するリソースが混在するという状況になってしまいます。
そこで、Terraformを利用してEKSの環境も構築すれば、シンプルなのではと思い、作ってみました。

なお、EKSのマネージドノードグループは利用せずに自分でノードグループを作成するようにしています。
そちらの方が色々カスタマイズできるためです。

概要

  • EKSのリソースを作る
  • Kubernetesのリソースを作る

全てのコードはGitHubにあります。 github.com

構築手順

EKSのリソースを作る

main.tf

locals {
  vpc_cidr_block  = "10.0.0.0/16"
  key_name        = "your_key_name"
  base_name       = "eks-example"
  cluster_name    = "${local.base_name}-cluster"
  region          = "ap-northeast-1"
  cluster_version = "1.15"
}

provider "aws" {
  region = local.region
}

key_nameにはEC2のキーペア名を指定します。


EKS用のVPCを作ります。

vpc.tf

data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_vpc" "vpc" {
  cidr_block           = local.vpc_cidr_block
  enable_dns_hostnames = true
  enable_dns_support   = true
  tags = {
    Name                                          = "${local.base_name}-vpc"
    "kubernetes.io/cluster/${local.cluster_name}" = "shared"
  }
}

resource "aws_subnet" "public_1" {
  vpc_id                  = aws_vpc.vpc.id
  cidr_block              = cidrsubnet(local.vpc_cidr_block, 4, 0)
  map_public_ip_on_launch = true
  availability_zone       = data.aws_availability_zones.available.names[0]
  tags = {
    Name                                          = "${local.base_name}-subnet-1"
    "kubernetes.io/cluster/${local.cluster_name}" = "shared"
  }
}

resource "aws_subnet" "public_2" {
  vpc_id                  = aws_vpc.vpc.id
  cidr_block              = cidrsubnet(local.vpc_cidr_block, 4, 1)
  map_public_ip_on_launch = true
  availability_zone       = data.aws_availability_zones.available.names[1]
  tags = {
    Name                                          = "${local.base_name}-subnet-2"
    "kubernetes.io/cluster/${local.cluster_name}" = "shared"
  }
}

resource "aws_subnet" "public_3" {
  vpc_id                  = aws_vpc.vpc.id
  cidr_block              = cidrsubnet(local.vpc_cidr_block, 4, 2)
  map_public_ip_on_launch = true
  availability_zone       = data.aws_availability_zones.available.names[2]
  tags = {
    Name                                          = "${local.base_name}-subnet-3"
    "kubernetes.io/cluster/${local.cluster_name}" = "shared"
  }
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${local.base_name}-igw"
  }
}

resource "aws_route_table" "rtb" {
  vpc_id = aws_vpc.vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "${local.base_name}-rtb"
  }
}

resource "aws_route_table_association" "rtba-1" {
  subnet_id      = aws_subnet.public_1.id
  route_table_id = aws_route_table.rtb.id
}

resource "aws_route_table_association" "rtba-2" {
  subnet_id      = aws_subnet.public_2.id
  route_table_id = aws_route_table.rtb.id
}

resource "aws_route_table_association" "rtba-3" {
  subnet_id      = aws_subnet.public_3.id
  route_table_id = aws_route_table.rtb.id
}

パブリックサブネットを3つ用意しています。
VPC、サブネットには以下のタグ指定が必要です。

kubernetes.io/cluster/<cluster-name> = shared

サブネットのサイズはPodの起動数に影響するので考慮が必要かもしれません。


クラスター、ノードのIAMロールを作ります。

iam.tf

resource "aws_iam_role" "eks_master_role" {
  name = "eks-master-role"

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

resource "aws_iam_role_policy_attachment" "eks-cluster-policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
  role       = aws_iam_role.eks_master_role.name
}

resource "aws_iam_role" "eks_node_role" {
  name = "eks-node-role"

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

resource "aws_iam_role_policy_attachment" "eks_worker_node_policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
  role       = aws_iam_role.eks_node_role.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_role.name
}

resource "aws_iam_role_policy_attachment" "ec2_container_registry_readonly" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
  role       = aws_iam_role.eks_node_role.name
}

resource "aws_iam_instance_profile" "eks_node_role_profile" {
  name = "eks-node-role-profile"
  role = aws_iam_role.eks_node_role.name
}

クラスターにはAmazonEKSClusterPolicy、ノードにはAmazonEKSWorkerNodePolicyAmazonEKS_CNI_PolicyAmazonEC2ContainerRegistryReadOnly のポリシーを付与します。


EKSクラスタを作成します。

eks.tf

resource "aws_eks_cluster" "eks_cluster" {
  name     = local.cluster_name
  role_arn = aws_iam_role.eks_master_role.arn
  version  = local.cluster_version

  vpc_config {
    subnet_ids = [
      aws_subnet.public_1.id,
      aws_subnet.public_2.id,
      aws_subnet.public_3.id,
    ]
  }

  depends_on = [
    aws_iam_role_policy_attachment.eks-cluster-policy,
  ]

  provisioner "local-exec" {
    command = <<EOT
    until curl -k -s ${aws_eks_cluster.eks_cluster.endpoint}/healthz >/dev/null; do sleep 4; done
  EOT
  }
}

depends_onでポリシーの作成完了後にクラスタを作成するように指定します。
クラスタの作成完了を待つ必要があるため、provisionerを設定しています。
これがないと、この後のKubernetesリソースを作る処理に失敗するという問題があったための対応です。


Auto Scaling Group、Launch Configurationを作成します。

asg.tf

locals {
  userdata = <<USERDATA
#!/bin/bash
set -o xtrace
/etc/eks/bootstrap.sh "${aws_eks_cluster.eks_cluster.name}"
USERDATA
}

data "aws_ami" "eks_node" {
  most_recent = true
  owners      = ["602401143452"]

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

resource "aws_autoscaling_group" "eks_asg" {
  name                 = "EKS cluster nodes"
  desired_capacity     = 2
  launch_configuration = aws_launch_configuration.eks_lc.id
  max_size             = 2
  min_size             = 2

  vpc_zone_identifier = [
    aws_subnet.public_1.id,
    aws_subnet.public_2.id,
    aws_subnet.public_3.id,
  ]

  tag {
    key                 = "Name"
    value               = "${local.base_name}-nodes"
    propagate_at_launch = true
  }

  tag {
    key                 = "kubernetes.io/cluster/${local.cluster_name}"
    value               = "owned"
    propagate_at_launch = true
  }

  lifecycle {
    create_before_destroy = true
  }

}

resource "aws_launch_configuration" "eks_lc" {
  associate_public_ip_address = true
  iam_instance_profile        = aws_iam_instance_profile.eks_node_role_profile.id
  image_id                    = data.aws_ami.eks_node.image_id
  instance_type               = "t3.small"
  name_prefix                 = "eks-node"
  key_name                    = local.key_name
  enable_monitoring           = false

  root_block_device {
    volume_type = "gp2"
    volume_size = "20"
  }

  security_groups = [
    aws_eks_cluster.eks_cluster.vpc_config[0].cluster_security_group_id,
  ]
  user_data_base64 = base64encode(local.userdata)

  lifecycle {
    create_before_destroy = true
  }

}

ノードには以下のタグを追加する必要があります。

kubernetes.io/cluster/<cluster-name> = owned

userdataではEKSクラスタとノードがつながるようにbootstrap.shを動かすようにしています。

launch configurationに設定するセキュリティグループはクラスタ作成時に作られるセキュリティグループを設定します。
Amazon EKS セキュリティグループの考慮事項 - Amazon EKS

使用するAMIは公式のものです。
Amazon EKS 最適化 Linux AMI - Amazon EKS

Kubernetesのリソースを作る

EKSクラスタをセルフマネージドノードで作成する場合、EKSのリソースを作成するだけでなく、Kubernetesのリソースを作る必要があります。
そのため、TerraformのKubernetesプロバイダを使用して、Kubernetesのリソースを作ります。

aws-auth.tf

data "aws_eks_cluster_auth" "cluster" {
  name = local.cluster_name
}

provider "kubernetes" {
  host                   = aws_eks_cluster.eks_cluster.endpoint
  token                  = data.aws_eks_cluster_auth.cluster.token
  cluster_ca_certificate = base64decode(aws_eks_cluster.eks_cluster.certificate_authority.0.data)
  load_config_file       = false
}

resource "kubernetes_config_map" "main" {
  metadata {
    name      = "aws-auth"
    namespace = "kube-system"
  }

  data = {
    mapRoles = <<YAML
- rolearn: ${aws_iam_role.eks_node_role.arn}
  username: system:node:{{EC2PrivateDNSName}}
  groups:
    - system:bootstrappers
    - system:nodes
YAML
  }

  depends_on = [aws_eks_cluster.eks_cluster]
}

aws-authというconfigmapが必要になるので、作成します。
これによって、EKSのクラスタがノードを認識するようになります。

Amazon EKS Linux ワーカーノードの起動 - Amazon EKS

まとめ

とりあえず作ってはみましたが、TerraformでEKSの環境を作る場合、結構手間が多い印象です。
今後のEKSアップデートに伴い、Terraformのリソースをそのアップデートに追従していくのは大変な気がします。
EKSのマネージドノードグループのアップデートによって、セルフマネージドノードグループが不要になれば、Terraformでも良くなるかもしれないです。
CloudFormationを使っていれば、eksctlはCloudFormationのリソースを作っているだけなので、現状でもうまいこと併用できるかもしれません。

参考

TerraformでEKSを作成するTerraform moduleがあります。
手っ取り早く作りたい場合、こちらを使用した方が良いでしょう。

registry.terraform.io