LoginSignup
16
12

Kubernetes初心者が今日から始めるEKS on Fargate(その1:サービスを公開する)

Last updated at Posted at 2021-05-03

はじめに

コンテナオーケストレーションツールと言えば、パブリッククラウドのプロバイダが提供するマネージドサービスを除けば Kubernetes がほぼデファクトスタンダードになってきたと言っても過言ではないだろう。
そこで、そろそろ真面目に Kubernetes を勉強しようにも最初の一歩で何をしたら良いか躓いている人も多いと思う。
本記事では、そんな感じで Kubernetes やってみようぜ!な初学者向けに書いてみる。
これでいいのか?という気はするが、Kubernetes のコントロールプレーンのインストール等の手間を省くことを考慮して、EKS を使ってみる。

一方で、コンテナやパブリッククラウドに関しての完全な初学者向けに書くのはなかなか難しいため、以下の知識があることを前提にする。

  • ECS でのコンテナ管理を少しでもしたことがある
  • Terraform をそこそこ書いたことがある

Terraform は別になくても良いのだけど、せっかくコンテナを使うからには冪等に作るべきなので、本記事では、terraform apply の一撃で、EKS on Fargate な Nginx のサービスを起動して ALB 経由でインターネットからアクセスできるようにすることを目標にする。

また、今回はひとまず動かすところまでを優先しているため、セキュリティグループの設定はデフォルトのまま(EKS 作成時に勝手に作られるものと、ALB 作成時に勝手につくられるものから変更していない)としている。必要に応じて修正をしていただきたい。

最初に結論

先に結論を書いておく。

ECS はすごく良くできてると改めて感じた。
少なくとも、今 ECS を使っている人が、パブリッククラウドのプロバイダを変更することを前提にすること以外に、ECS → EKS に乗り換えるメリットというのはあまり無いように感じる(それとて、どうせ乗り換えるならあえて EKS にするくらいであれば、さっさと乗り換えてしまった方が良いだろう)。
これからコンテナをゼロから学ぶというのであれば、Kubernetes は選択肢になり得ると思う。ただし、ECS のように AWS に完全統合された環境ではないので、覚えることは ECS よりも多くなるだろう。

あと、素で EKS を触ると大変すぎて死ねるので、eksctl (コマンドラインからEKSを制御するコマンド)は必須だろう。ちゃんとインストールしておこう。というか、仕組みを理解したら、その後は eksctl を使わない理由はあまり無いと思う。

2021/8/11追記
上記で eksctl を使わない理由が無いと言いつつ、eksctl は中で CloudFormation を動かしてしまうため、やはりリソース管理の上ではつらみがある。Terraform と組み合わせると、terraform, eksctl, cloudformation, kubectl が作るリソースを頑張って整合するように考えなければいけなくなる。本記事では、後半に追記分として、eksctl に頼らずになるべく terraform でリソース作成をして、どうしても無理な部分だけ kubectl にやらせるという方法も紹介する。

全体構成

今回の Terraform では以下のリソースを作成する。

  • VPC、サブネット
  • EKS クラスタ、Fargate プロファイル
  • OIDCプロバイダ
  • ALB Ingress Controller(Kubernetesのリソース)

VPCはありものを使っても良いが、EKS がいろいろバックエンドでゴニョゴニョやるための識別子をタグに埋め込む必要がある。既存の環境を汚したくない人は、新規でVPCを作っておいた方が良いだろう。

また、途中で使うコマンドの都合上、terraform destroy で消せないリソースが登場するので注意。
そういったリソースには本記事中でも注記をすることにする。

ネットワークを作る

ここはそんなに難しいことはない。EKS の制約上、最低でも2つ以上のプライベートネットワークが必要になるので、それぞれ作って Nat Gateway をアタッチしておこう。

また、VPC とサブネットのリソースには "kubernetes.io/cluster/${local.eks_cluster_name}" = "shared" のタグを付与しておこう。これを忘れるとうまく動いてくれない。パブリックサブネットには "kubernetes.io/role/elb" = "1"、プライベートサブネットには "kubernetes.io/role/internal-elb" = "1" を付与しておく。

################################################################################
# VPC                                                                          #
################################################################################
resource "aws_vpc" "for_eks_fargate" {
  cidr_block           = "192.168.0.0/16"
  instance_tenancy     = "default"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name                                              = local.vpc_name
    "kubernetes.io/cluster/${local.eks_cluster_name}" = "shared"
  }
}

################################################################################
# Public Subnet                                                                #
################################################################################
resource "aws_subnet" "public1" {
  vpc_id                  = aws_vpc.for_eks_fargate.id
  cidr_block              = "192.168.0.0/24"
  map_public_ip_on_launch = true
  availability_zone       = "ap-northeast-1a"

  tags = {
    "Name"                                            = local.public_subnet_name1
    "kubernetes.io/cluster/${local.eks_cluster_name}" = "shared"
    "kubernetes.io/role/elb"                          = "1"
  }
}

resource "aws_subnet" "public2" {
  vpc_id                  = aws_vpc.for_eks_fargate.id
  cidr_block              = "192.168.1.0/24"
  map_public_ip_on_launch = true
  availability_zone       = "ap-northeast-1c"

  tags = {
    "Name"                                            = local.public_subnet_name2
    "kubernetes.io/cluster/${local.eks_cluster_name}" = "shared"
    "kubernetes.io/role/elb"                          = "1"
  }
}

################################################################################
# Private Subnet                                                               #
################################################################################
resource "aws_subnet" "private1" {
  vpc_id                  = aws_vpc.for_eks_fargate.id
  cidr_block              = "192.168.2.0/24"
  map_public_ip_on_launch = false
  availability_zone       = "ap-northeast-1a"

  tags = {
    "Name"                                            = local.private_subnet_name1
    "kubernetes.io/cluster/${local.eks_cluster_name}" = "shared"
    "kubernetes.io/role/internal-elb"                 = "1"
  }
}

resource "aws_subnet" "private2" {
  vpc_id                  = aws_vpc.for_eks_fargate.id
  cidr_block              = "192.168.3.0/24"
  map_public_ip_on_launch = false
  availability_zone       = "ap-northeast-1c"

  tags = {
    "Name"                                            = local.private_subnet_name2
    "kubernetes.io/cluster/${local.eks_cluster_name}" = "shared"
    "kubernetes.io/role/internal-elb"                 = "1"
  }
}

################################################################################
# Internet Gateway                                                             #
################################################################################
resource "aws_internet_gateway" "for_eks_fargate" {
  vpc_id = aws_vpc.for_eks_fargate.id

  tags = {
    "Name" = local.igw_name
  }
}

################################################################################
# EIP                                                                          #
################################################################################
resource "aws_eip" "for_nat_gateway1" {
  vpc = true

  tags = {
    Name = local.eip_name1
  }
}

resource "aws_eip" "for_nat_gateway2" {
  vpc = true

  tags = {
    Name = local.eip_name2
  }
}

################################################################################
# Nat Gateway                                                                  #
################################################################################
resource "aws_nat_gateway" "for_eks_fargate1" {
  depends_on = [aws_internet_gateway.for_eks_fargate]

  subnet_id     = aws_subnet.public1.id
  allocation_id = aws_eip.for_nat_gateway1.id

  tags = {
    Name = local.ngw_name1
  }
}

resource "aws_nat_gateway" "for_eks_fargate2" {
  depends_on = [aws_internet_gateway.for_eks_fargate]

  subnet_id     = aws_subnet.public2.id
  allocation_id = aws_eip.for_nat_gateway2.id

  tags = {
    Name = local.ngw_name2
  }
}

################################################################################
# Route Table                                                                  #
################################################################################
resource "aws_route_table" "public1" {
  vpc_id = aws_vpc.for_eks_fargate.id
}

resource "aws_route" "public1" {
  route_table_id         = aws_route_table.public1.id
  gateway_id             = aws_internet_gateway.for_eks_fargate.id
  destination_cidr_block = "0.0.0.0/0"
}

resource "aws_route_table_association" "public1" {
  subnet_id      = aws_subnet.public1.id
  route_table_id = aws_route_table.public1.id
}

resource "aws_route_table" "public2" {
  vpc_id = aws_vpc.for_eks_fargate.id
}

resource "aws_route" "public2" {
  route_table_id         = aws_route_table.public2.id
  gateway_id             = aws_internet_gateway.for_eks_fargate.id
  destination_cidr_block = "0.0.0.0/0"
}

resource "aws_route_table_association" "public2" {
  subnet_id      = aws_subnet.public2.id
  route_table_id = aws_route_table.public2.id
}

resource "aws_route_table" "private1" {
  vpc_id = aws_vpc.for_eks_fargate.id
}

resource "aws_route" "private1" {
  route_table_id         = aws_route_table.private1.id
  nat_gateway_id         = aws_nat_gateway.for_eks_fargate1.id
  destination_cidr_block = "0.0.0.0/0"
}

resource "aws_route_table_association" "private1" {
  subnet_id      = aws_subnet.private1.id
  route_table_id = aws_route_table.private1.id
}

resource "aws_route_table" "private2" {
  vpc_id = aws_vpc.for_eks_fargate.id
}

resource "aws_route" "private2" {
  route_table_id         = aws_route_table.private2.id
  nat_gateway_id         = aws_nat_gateway.for_eks_fargate2.id
  destination_cidr_block = "0.0.0.0/0"
}

resource "aws_route_table_association" "private2" {
  subnet_id      = aws_subnet.private2.id
  route_table_id = aws_route_table.private2.id
}

IAM の準備

EKS クラスタと Pod 実行用のサービスロールが必要になるので作成しておく。
それぞれ AWS マネージドな IAM ポリシーがあるので、それをアタッチすれば良いだろう。

################################################################################
# IAM Role for EKS Cluster                                                     #
################################################################################
resource "aws_iam_role" "ekscluster" {
  name               = local.ekscluster_role_name
  assume_role_policy = data.aws_iam_policy_document.ekscluster_assume.json
}

data "aws_iam_policy_document" "ekscluster_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "eks.amazonaws.com",
      ]
    }
  }
}

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

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

################################################################################
# IAM Role for EKS Pod Execution                                               #
################################################################################
resource "aws_iam_role" "ekspodexecution" {
  name               = local.ekspodexecution_role_name
  assume_role_policy = data.aws_iam_policy_document.ekspodexecution_assume.json
}

data "aws_iam_policy_document" "ekspodexecution_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "eks-fargate-pods.amazonaws.com",
      ]
    }
  }
}

resource "aws_iam_role_policy_attachment" "ekspodexecution1" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy"
  role       = aws_iam_role.ekspodexecution.name
}

EKS クラスタの作成

いよいよ EKS クラスタの作成だ。
ここで必要になるのは、aws_eks_clusteraws_eks_fargate_profile だ。
aws_cloudwatch_log_group についてはログ採取が必要な場合に設定する。何も指定しなくても設定されるが、保存期間が無制限になってしまうので、予め EKS 指定の名前で作っておくことで、Terraform の制御下に置くことができる。

aws_eks_cluster については、明示的にポリシーのアタッチとの順番を制御する必要があるため、depends_on でポリシーのアタッチが先に完了するようにしておく。vpc_config については、パブリック、プライベート問わず作成したすべてのサブネットを設定する。
aws_eks_fargate_profilesubnet_ids については、バックエンドのノードを作るサブネットなので、プライベートのみで良い。

################################################################################
# EKS                                                                          #
################################################################################
resource "aws_eks_cluster" "example" {
  depends_on = [
    aws_iam_role_policy_attachment.ekscluster1,
    aws_iam_role_policy_attachment.ekscluster2,
    aws_cloudwatch_log_group.eks_cluster,
  ]

  name     = local.eks_cluster_name
  role_arn = aws_iam_role.ekscluster.arn
  version  = "1.19"

  vpc_config {
    subnet_ids = [
      aws_subnet.public1.id,
      aws_subnet.public2.id,
      aws_subnet.private1.id,
      aws_subnet.private2.id,
    ]
  }

  enabled_cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
}

resource "aws_eks_fargate_profile" "kubesystem" {
  cluster_name           = aws_eks_cluster.example.name
  fargate_profile_name   = local.eks_fargate_kubesystem_profile_name
  pod_execution_role_arn = aws_iam_role.ekspodexecution.arn
  subnet_ids             = [aws_subnet.private1.id, aws_subnet.private2.id]

  selector {
    namespace = "default"
  }

  selector {
    namespace = "kube-system"
  }
}

resource "aws_cloudwatch_log_group" "eks_cluster" {
  name              = "/aws/eks/${local.eks_cluster_name}/cluster"
  retention_in_days = 3
}

Kubernetes が必要とするYAMLを作成する

さて、ここまでは大したことない(実際、EKS on EC2 であればだいたいこれで終わり)のだが、ここからが大変な部分だ。
Kubernetes を制御するための YAML を作成していく。

必要なものは以下。

  • Kubernetes の Config ファイル
  • ALB Ingress Controller の Manifest ファイル
  • Kubernetes に設定するロールの Manifest ファイル
  • Nginx コンテナを起動するための Manifest ファイル

それぞれ、AWS リソースを埋め込む必要があるため、以下のような感じで template_file を使って自動で作成する。

ちなみに、この節の元ネタはAWSのブログなのだが、コピペ元のYAMLファイルが壊れていたり、既にこのブログが書かれたときから時間がったっていてすでに使えない apiVersion があったりして、かなり大変だった……。1年ちょっと前の記事にもかかわらず既に deprecated だったり使えなくなったりする構文があるというライフサイクルの速さは、EKS がしんどいと言われる所以でもある……。

################################################################################
# Local File for Kubernetes Config                                             #
################################################################################
resource "local_file" "kubeconfig" {
  filename = "./output_files/kubeconfig.yaml"
  content  = data.template_file.kubeconfig.rendered
}

data "template_file" "kubeconfig" {
  template = file("${path.module}/kubernetes_template/01_kubeconfig_template.yaml")

  vars = {
    eks_certificate_authority_data = aws_eks_cluster.example.certificate_authority.0.data
    eks_cluster_endpoint           = aws_eks_cluster.example.endpoint
    eks_cluster_arn                = aws_eks_cluster.example.arn
    eks_cluster_region             = data.aws_region.current.name
    eks_cluster_name               = local.eks_cluster_name
  }
}

################################################################################
# Local File for ALB Ingress Controller                                        #
################################################################################
resource "local_file" "alb_ingress_controller" {
  filename = "./output_files/alb-ingress-controller.yaml"
  content  = data.template_file.alb_ingress_controller.rendered
}

data "template_file" "alb_ingress_controller" {
  template = file("${path.module}/kubernetes_template/11_alb-ingress-controller.yaml")

  vars = {
    eks_cluster_name = aws_eks_cluster.example.name
    vpc_id           = aws_vpc.for_eks_fargate.id
    region_name      = data.aws_region.current.name
  }
}

################################################################################
# Local File for RBAC Role                                                     #
################################################################################
resource "local_file" "rbac_role" {
  filename = "./output_files/rbac-role.yaml"
  content  = data.template_file.rbac_role.rendered
}

data "template_file" "rbac_role" {
  template = file("${path.module}/kubernetes_template/12_rbac-role.yaml")
}

################################################################################
# Local File for Nginx Deployment                                              #
################################################################################
resource "local_file" "nginx_deployment" {
  filename = "./output_files/nginx-deployment.yaml"
  content  = data.template_file.nginx_deployment.rendered
}

data "template_file" "nginx_deployment" {
  template = file("${path.module}/kubernetes_template/13_nginx-deployment.yaml")

  vars = {
    eks_fargate_profile_name = aws_eks_fargate_profile.kubesystem.fargate_profile_name
  }
}

################################################################################
# Local File for Nginx Service                                                 #
################################################################################
resource "local_file" "nginx_service" {
  filename = "./output_files/nginx-service.yaml"
  content  = data.template_file.nginx_service.rendered
}

data "template_file" "nginx_service" {
  template = file("${path.module}/kubernetes_template/14_nginx-service.yaml")
}

################################################################################
# Local File for Nginx Ingress                                                 #
################################################################################
resource "local_file" "nginx_ingress" {
  filename = "./output_files/nginx-ingress.yaml"
  content  = data.template_file.nginx_ingress.rendered
}

data "template_file" "nginx_ingress" {
  template = file("${path.module}/kubernetes_template/15_nginx-ingress.yaml")
}
01_kubeconfig_template.yaml
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: ${eks_certificate_authority_data}
    server: ${eks_cluster_endpoint}
  name: ${eks_cluster_arn}
contexts:
- context:
    cluster: ${eks_cluster_arn}
    user: ${eks_cluster_arn}
  name: ${eks_cluster_arn}
current-context: ${eks_cluster_arn}
kind: Config
preferences: {}
users:
- name: ${eks_cluster_arn}
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1alpha1
      args:
      - --region
      - ${eks_cluster_region}
      - eks
      - get-token
      - --cluster-name
      - ${eks_cluster_name}
      command: aws
11_alb-ingress-controller.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: kube-system
  name: alb-ingress-controller
  labels:
    app.kubernetes.io/name: alb-ingress-controller
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: alb-ingress-controller
  template:
    metadata:
      labels:
        app.kubernetes.io/name: alb-ingress-controller
    spec:
      containers:
      - name: alb-ingress-controller
        args:
        - --ingress-class=alb
        - --cluster-name=${eks_cluster_name}
        - --aws-vpc-id=${vpc_id}
        - --aws-region=${region_name}
        image: docker.io/amazon/aws-alb-ingress-controller:v1.1.4
      serviceAccountName: alb-ingress-controller
12_rbac-role.yaml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: alb-ingress-controller
  name: alb-ingress-controller
rules:
  - apiGroups:
      - ""
      - extensions
    resources:
      - configmaps
      - endpoints
      - events
      - ingresses
      - ingresses/status
      - services
    verbs:
      - create
      - get
      - list
      - update
      - watch
      - patch
  - apiGroups:
      - ""
      - extensions
    resources:
      - nodes
      - pods
      - secrets
      - services
      - namespaces
    verbs:
      - get
      - list
      - watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  labels:
    app.kubernetes.io/name: alb-ingress-controller
  name: alb-ingress-controller
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: alb-ingress-controller
subjects:
  - kind: ServiceAccount
    name: alb-ingress-controller
    namespace: kube-system
13_nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: default
  name: nginx-deployment
  labels:
    eks.amazonaws.com/fargate-profile: ${eks_fargate_profile_name}
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx:1.20
        imagePullPolicy: Always
        name: nginx
        ports:
        - containerPort: 80
14_nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
  namespace: "default"
  name: "nginx-service"
  annotations:
    alb.ingress.kubernetes.io/target-type: ip
spec:
  selector:
    app: "nginx"
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
  type: NodePort
15_nginx-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: default
  name: nginx-ingress
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
  labels:
    app: nginx-ingress
spec:
  rules:
  - http:
      paths:
      - path: /*
        pathType: Prefix
        backend:
          service:
            name: nginx-service
            port:
              number: 80

CoreDNS を Fargate 向けに書き換える

さて、素で起動してきた EKS Cluster は、DNS をEC2 で起動しようとして進まなくなる。
これを Fargate に向けてあげる必要がある。
コマンド自体は AWS 公式のユーザーガイド参照。
今回は、冪等性を高めるために null_resource を使って自動でパッチをして再起動する。

resource "null_resource" "coredns_patch" {
  depends_on = [
    aws_eks_fargate_profile.kubesystem,
    local_file.kubeconfig,
    local_file.alb_ingress_controller,
    local_file.rbac_role,
    local_file.nginx_deployment,
    local_file.nginx_ingress,
    local_file.nginx_service,
  ]

  provisioner "local-exec" {
    environment = {
      KUBECONFIG = local_file.kubeconfig.filename
    }
    command = "kubectl patch deployment coredns -n kube-system --type json -p='[{\"op\": \"remove\", \"path\": \"/spec/template/metadata/annotations/eks.amazonaws.com~1compute-type\"}]'"

    on_failure = fail
  }
}

resource "null_resource" "coredns_restart" {
  depends_on = [null_resource.coredns_patch]

  provisioner "local-exec" {
    environment = {
      KUBECONFIG = local_file.kubeconfig.filename
    }
    command = "kubectl rollout restart -n kube-system deployment coredns"

    on_failure = fail
  }
}

ALB を構築する

本記事を書いた2021年8月時点では、ALB Ingress Controllerを使うのがプラクティスとされていたが、現在では非推奨とされている。2023年11月時点では、後述のAWS Load Balancer Controllerを使用するのがセオリーとされている。

これでサービス公開の準備はほぼ整った。あとは、実際に ALB を構築してコンテナをデプロイしていく。
ここも AWS 公式のブログが元ネタで、null_resource で自動化をしていく。

まずは、ALB の IAM 権限を制御できるように以下のIDプロバイダと IAM ポリシーを作成する。

data "tls_certificate" "for_eks_fargate_pod" {
  url = aws_eks_cluster.example.identity[0].oidc[0].issuer
}

resource "aws_iam_openid_connect_provider" "for_eks_fargate_pod" {
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = [data.tls_certificate.for_eks_fargate_pod.certificates[0].sha1_fingerprint]
  url             = aws_eks_cluster.example.identity[0].oidc[0].issuer
}

resource "aws_iam_policy" "alb_ingress_controller" {
  name   = local.eksalbingresscontroller_policy_name
  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "acm:DescribeCertificate",
                "acm:ListCertificates",
                "acm:GetCertificate"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:CreateSecurityGroup",
                "ec2:CreateTags",
                "ec2:DeleteTags",
                "ec2:DeleteSecurityGroup",
                "ec2:DescribeAccountAttributes",
                "ec2:DescribeAddresses",
                "ec2:DescribeInstances",
                "ec2:DescribeInstanceStatus",
                "ec2:DescribeInternetGateways",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeSubnets",
                "ec2:DescribeTags",
                "ec2:DescribeVpcs",
                "ec2:ModifyInstanceAttribute",
                "ec2:ModifyNetworkInterfaceAttribute",
                "ec2:RevokeSecurityGroupIngress"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:AddListenerCertificates",
                "elasticloadbalancing:AddTags",
                "elasticloadbalancing:CreateListener",
                "elasticloadbalancing:CreateLoadBalancer",
                "elasticloadbalancing:CreateRule",
                "elasticloadbalancing:CreateTargetGroup",
                "elasticloadbalancing:DeleteListener",
                "elasticloadbalancing:DeleteLoadBalancer",
                "elasticloadbalancing:DeleteRule",
                "elasticloadbalancing:DeleteTargetGroup",
                "elasticloadbalancing:DeregisterTargets",
                "elasticloadbalancing:DescribeListenerCertificates",
                "elasticloadbalancing:DescribeListeners",
                "elasticloadbalancing:DescribeLoadBalancers",
                "elasticloadbalancing:DescribeLoadBalancerAttributes",
                "elasticloadbalancing:DescribeRules",
                "elasticloadbalancing:DescribeSSLPolicies",
                "elasticloadbalancing:DescribeTags",
                "elasticloadbalancing:DescribeTargetGroups",
                "elasticloadbalancing:DescribeTargetGroupAttributes",
                "elasticloadbalancing:DescribeTargetHealth",
                "elasticloadbalancing:ModifyListener",
                "elasticloadbalancing:ModifyLoadBalancerAttributes",
                "elasticloadbalancing:ModifyRule",
                "elasticloadbalancing:ModifyTargetGroup",
                "elasticloadbalancing:ModifyTargetGroupAttributes",
                "elasticloadbalancing:RegisterTargets",
                "elasticloadbalancing:RemoveListenerCertificates",
                "elasticloadbalancing:RemoveTags",
                "elasticloadbalancing:SetIpAddressType",
                "elasticloadbalancing:SetSecurityGroups",
                "elasticloadbalancing:SetSubnets",
                "elasticloadbalancing:SetWebAcl"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:CreateServiceLinkedRole",
                "iam:GetServerCertificate",
                "iam:ListServerCertificates"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cognito-idp:DescribeUserPoolClient"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "waf-regional:GetWebACLForResource",
                "waf-regional:GetWebACL",
                "waf-regional:AssociateWebACL",
                "waf-regional:DisassociateWebACL"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "tag:GetResources",
                "tag:TagResources"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "waf:GetWebACL"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "wafv2:GetWebACL",
                "wafv2:GetWebACLForResource",
                "wafv2:AssociateWebACL",
                "wafv2:DisassociateWebACL"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "shield:DescribeProtection",
                "shield:GetSubscriptionState",
                "shield:DeleteProtection",
                "shield:CreateProtection",
                "shield:DescribeSubscription",
                "shield:ListProtections"
            ],
            "Resource": "*"
        }
    ]
}
EOF
}

そのうえで、以下のように Kubernetes のロールの設定と、IAM の紐づけを行う。

resource "null_resource" "create_rbac_role" {
  depends_on = [null_resource.coredns_restart]

  provisioner "local-exec" {
    environment = {
      KUBECONFIG = local_file.kubeconfig.filename
    }
    command = "kubectl apply -f ./output_files/rbac-role.yaml"

    on_failure = fail
  }
}

resource "null_resource" "create_iamserviceaccount" {
  depends_on = [null_resource.create_rbac_role]

  provisioner "local-exec" {
    command = "eksctl create iamserviceaccount --name alb-ingress-controller --namespace kube-system --cluster ${aws_eks_cluster.example.name} --attach-policy-arn ${aws_iam_policy.alb_ingress_controller.arn} --approve"

    on_failure = fail
  }
}

ここで CloudFormation のスタックが作成され、そこから IAM ロールが作成&ポリシーへのアタッチが行われる。
スタックを削除しないと、IAM の削除ができずに terraform destroy が失敗するので注意が必要だ。

ALB を作成する準備ができたら、あとは↑で作った Manifest ファイルを、kubectl apply で流していけば良い。

resource "null_resource" "create_alb_ingress_controller" {
  depends_on = [null_resource.create_iamserviceaccount]

  provisioner "local-exec" {
    environment = {
      KUBECONFIG = local_file.kubeconfig.filename
    }
    command = "kubectl apply -f ./output_files/alb-ingress-controller.yaml"

    on_failure = fail
  }
}

resource "null_resource" "nginx_service" {
  depends_on = [null_resource.create_alb_ingress_controller]

  provisioner "local-exec" {
    environment = {
      KUBECONFIG = local_file.kubeconfig.filename
    }
    command = "kubectl apply -f ./output_files/nginx-service.yaml"

    on_failure = fail
  }
}

resource "null_resource" "nginx_deployment" {
  depends_on = [null_resource.nginx_service]

  provisioner "local-exec" {
    environment = {
      KUBECONFIG = local_file.kubeconfig.filename
    }
    command = "kubectl apply -f ./output_files/nginx-deployment.yaml"

    on_failure = fail
  }
}

resource "null_resource" "nginx_ingress" {
  depends_on = [null_resource.nginx_deployment]

  provisioner "local-exec" {
    environment = {
      KUBECONFIG = local_file.kubeconfig.filename
    }
    command = "kubectl apply -f ./output_files/nginx-ingress.yaml"

    on_failure = fail
  }
}

うまく作れると、ALB が作成され、そのURLへのアクセスで Nginx の画面が開けるはずだ。

キャプチャ1.png

ちなみに、この ALB を作る過程で、AWSリソースとしての ALB、ターゲットグループ、セキュリティグループが自動で作成される。
これらを削除しないと、terraform destroy で VPC が削除できずエラーになるので注意が必要だ。

この ALB はマネージメントコンソールから確認できるものの、ちゃんと負荷状況に応じてスケールしてくれるかが分からない。
ALB がボトルネックになってしまったら元も子もないので、このあたりは以降でしっかり検証していきたい。

2021/8/11 追記 eksctl に頼らずALBを構築する

本記事を書いた2021年8月時点では、ALB Ingress Controllerを使うのがプラクティスとされていたが、現在では非推奨とされている。2023年11月時点では、後述のAWS Load Balancer Controllerを使用するのがセオリーとされている。

さて、これまで書いてきた通り、リソースの作成過程で Terraform 管理外の様々なリソースを作ってきているが、これを、ALB とターゲットグループのみに減らすことができる。

これにより、オペミスでリソースを消せなくなるリスクを極力下げることができる。
※そもそもリソースを消すということ自体があまり無いので、困らないかもしれないが。リソースのアップデートに対しても同様の事が言えるので、管理を寄せるのは悪いことではないだろう。

IAM の準備(追加分)

ALB を作る前段で、OIDC に関連したポリシーとプロバイダを作っているが、これをもとにIAMロールを eks 経由の CloudFormation で作成している。これを自力で作っておこう。

resource "aws_iam_role" "ekscluster_oidc" {
  name               = local.ekscluster_oidc_role_name
  assume_role_policy = data.aws_iam_policy_document.ekscluster_oidc_assume_policy.json

  tags = {
    "alpha.eksctl.io/cluster-name"                = aws_eks_cluster.example.name
    "eksctl.cluster.k8s.io/v1alpha1/cluster-name" = aws_eks_cluster.example.name
    "alpha.eksctl.io/iamserviceaccount-name"      = "kube-system/alb-ingress-controller"
    "alpha.eksctl.io/eksctl-version"              = "0.47.0"
  }
}

data "aws_iam_policy_document" "ekscluster_oidc_assume_policy" {
  statement {
    actions = ["sts:AssumeRoleWithWebIdentity"]
    effect  = "Allow"

    condition {
      test     = "StringEquals"
      variable = "${replace(aws_iam_openid_connect_provider.for_eks_fargate_pod.url, "https://", "")}:sub"
      values   = ["system:serviceaccount:kube-system:alb-ingress-controller"]
    }

    condition {
      test     = "StringEquals"
      variable = "${replace(aws_iam_openid_connect_provider.for_eks_fargate_pod.url, "https://", "")}:aud"
      values   = ["sts.amazonaws.com"]
    }

    principals {
      identifiers = [aws_iam_openid_connect_provider.for_eks_fargate_pod.arn]
      type        = "Federated"
    }
  }
}

resource "aws_iam_role_policy_attachment" "ekscluster_oidc" {
  role       = aws_iam_role.ekscluster_oidc.name
  policy_arn = aws_iam_policy.alb_ingress_controller.arn
}

eksctl している部分については、以下のようにマニフェストファイルを作って、コマンドを置き換えよう。

serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: alb-ingress-controller
  namespace: kube-system
  annotations:
    eks.amazonaws.com/role-arn: ${sa_role_arn}

上記の呼び出しについては、以下のようにしてロールの情報を Kubernetes に渡す。

resource "local_file" "serviceaccount" {
  filename = "./output_files/serviceaccount.yaml"
  content  = data.template_file.serviceaccount.rendered
}

data "template_file" "serviceaccount" {
  template = file("${path.module}/kubernetes_template/serviceaccount.yaml")

  vars = {
    sa_role_arn = aws_iam_role.ekscluster_oidc.arn
  }
}

これを、以下のように呼び出そう。

resource "null_resource" "create_iamserviceaccount" {
  depends_on = [null_resource.create_rbac_role]

  provisioner "local-exec" {
    environment = {
      KUBECONFIG = local_file.kubeconfig.filename
    }
    command    = "kubectl apply -f ./output_files/serviceaccount.yaml"

    on_failure = fail
  }
}

セキュリティグループの準備

15_nginx-ingress.yaml を kubectl apply する際にセキュリティグループを自動で準備してくれるが、これがなかなかうまく消えてくれないことがあり、冪等性が下がる原因となる。
このため、セキュリティグループは自前で作って、マニフェストに値を渡してあげることにしよう。

まずはセキュリティグループの定義である。

resource "aws_security_group" "for_eks_ingress" {
  name        = local.eks_ingress_sg_name
  description = "managed LoadBalancer securityGroup by ALB Ingress Controller"
  vpc_id      = aws_vpc.for_eks_fargate.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "TCP"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow ingress on port 80 from 0.0.0.0/0"
  }

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

  tags = {
    Name = local.eks_ingress_sg_name
  }
}

また、上記セキュリティグループを、EKS クラスタのセキュリティグループに紐づける必要があるため、以下の定義も入れておく。
aws_eks_cluster.example.vpc_config[0].cluster_security_group_id は自分では作ることができず、EKS に任せるしかないため、EKS クラスタの定義から値を引っ張ってくるのが必要だ。

resource "aws_security_group_rule" "for_eks_cluster_allow_eks_ingress" {
  security_group_id        = aws_eks_cluster.example.vpc_config[0].cluster_security_group_id
  description              = "for_eks_cluster_allow_eks_ingress"
  type                     = "ingress"
  from_port                = 0
  to_port                  = 65535
  protocol                 = "TCP"
  source_security_group_id = aws_security_group.for_eks_ingress.id
}

また、上記で作成したセキュリティグループを ALB に設定するために、15_nginx-ingress.yaml に、以下を追記する。
これで、nginx-ingress の kubectl apply 時にデフォルトのセキュリティグループ作成をせずに、自前で設定したセキュリティグループを使える。

  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/security-groups: ${eks_ingress_sg_id} # ★これを追記

nginx-ingress 作成用の YAML を生成する null_resource には、以下を追記しておこう。

  vars = {
    eks_ingress_sg_id = aws_security_group.for_eks_ingress.id
  }

さらに terraform destroy を冪等にするために

さて、ここまでやればリソースは Terraform に大部分寄せることができたが、terraform destroy 時にゴミが残る課題は残る。

これについては、terraform destroy 時だけ操作する null_resource を使用する。

resource "null_resource" "kubectl_delete" {
  depends_on = [
    aws_eks_cluster.example,
    aws_eks_fargate_profile.kubesystem,
    local_file.kubeconfig,
    local_file.create_namespace_awsobservablity,
    local_file.awslogging_cloudwatch_configmap,
    local_file.serviceaccount,
    local_file.alb_ingress_controller,
    local_file.rbac_role,
    local_file.nginx_service,
    local_file.nginx_deployment,
    local_file.nginx_ingress,
  ]

  triggers = {
    kubeconfig       = local_file.kubeconfig.filename
  }

  provisioner "local-exec" {
    when       = destroy
    on_failure = continue
    environment = {
      KUBECONFIG = self.triggers.kubeconfig
    }
    command = <<-EOF
      kubectl delete -f ./output_files/nginx-ingress.yaml --grace-period=0 --force &&
      sleep 30 &&
      kubectl delete -f ./output_files/nginx-deployment.yaml --grace-period=0 --force &&
      kubectl delete -f ./output_files/nginx-service.yaml --grace-period=0 --force &&
      kubectl delete -f ./output_files/alb-ingress-controller.yaml --grace-period=0 --force &&
      kubectl delete -f ./output_files/serviceaccount.yaml --grace-period=0 --force &&
      kubectl delete -f ./output_files/rbac-role.yaml --grace-period=0 --force &&
    EOF
  }
}

とりあえず、一通り作ったリソースは全部壊してから Terraform のリソース削除をしようという考えだ、
local_file に依存関係を作っているのは、内部コマンドでファイルを使っている手前、local_file が destroyされる(=kubectlできなくなる)のを防ぐためだ。

また、destroy 時の local-exec では、値の参照ができないという制約がある。
このため、無理矢理 triggers に値を入れて参照可能にしている。

あとは、kubectl delete --grace-period=0 --force で作ったリソースを順次消していく(強制削除しないとリソースが消えないことがある)。
nginx-ingress.yaml の後のみ sleep が入っているのは、リソース削除途中に次の kubectl リクエストを受けると、何か不整合になってしまうのか、ターゲットグループが消しきれないケースがあるためだ。
sleep 30 は不格好だが、他に方法を見つけられなかった……。
なお、ここまでやってもリソースが消えないケースがある。残念ながらそういう場合は、ALB ⇒ターゲットグループの順序で手動削除しよう(先に ALB を消さないとターゲットグループを消せない)。

なお、今回追記分は、eksctl を使わない前提で書いてきたが、eksctl を使う場合も、
kubectl delete -f ./output_files/serviceaccount.yaml --grace-period=0 --force &&
の部分を、

$ eksctl delete iamserviceaccount --name alb-ingress-controller --namespace kube-system --cluster eks-fargate-example-cluster 

な感じで削除することも可能だ。

これで、基本的に自由に作成削除の試行錯誤ができるようになったはずだ!

2023/11/26追記 AWS Load Balancer Controllerを使ってLB組み込みを制御する

AWS Load Balancer Controllerの概要についてはAWS公式のブログを見るのが良い。
ALB Ingress Controllerとの違いとして、ALB Ingress ControllerはこのリソースでALBを作成するが、AWS Load Balancer Controllerでは、ロードバランサーをKubernetesの制御外にして、通常のAWSリソースとして定義したものと連動をする。負荷分散機能とサービス提供機能の責任分担をより分かりやすくしたと考えられるだろう。

AWSリソースとしてのALBの定義

ALBはTerraformで以下のように通常通り作成すれば良い。
セキュリティグループには、通常のALBのフォワードに必要な設定を入れておこう。

################################################################################
# ALB                                                                          #
################################################################################0
resource "aws_lb" "example" {
  name               = local.alb_name
  load_balancer_type = "application"

  subnets = [
    aws_subnet.public1.id,
    aws_subnet.public2.id,
  ]

  security_groups = [
    aws_security_group.for_eks_ingress.id,
  ]

  tags = {
    "elbv2.k8s.aws/cluster"    = local.eks_cluster_name
    "ingress.k8s.aws/resource" = "LoadBalancer"
    "ingress.k8s.aws/stack"    = "default/nginx-ingress"
  }
}

resource "aws_lb_listener" "example" {
  load_balancer_arn = aws_lb.example.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.example.arn
  }
}

resource "aws_lb_target_group" "example" {
  name        = local.alb_tg_name
  vpc_id      = aws_vpc.for_eks_fargate.id
  port        = 80
  protocol    = "HTTP"
  target_type = "ip"

  tags = {
    "elbv2.k8s.aws/cluster"    = local.eks_cluster_name
    "ingress.k8s.aws/resource" = "default/nginx-ingress-nginx-service:80"
    "ingress.k8s.aws/stack"    = "default/nginx-ingress"
  }
}

IAMロールの設定

IAMロールは以下のように設定する。
Kubernetesのサービスアカウントと連動させるためのOpenIDConnectの設定が必要だ。
それぞれの設定は、公式のユーザーガイドのIAM設定を持ってきている。

################################################################################
# IAM Policy for AWS Load Balancer Controller                                  #
################################################################################
resource "aws_iam_role" "aws_loadbalancer_controller" {
  name               = local.eksawsloadbalancercontroller_role_name
  assume_role_policy = data.aws_iam_policy_document.aws_loadbalancer_controller_assume_policy.json

  tags = {
    "alpha.eksctl.io/cluster-name"                = aws_eks_cluster.example.name
    "eksctl.cluster.k8s.io/v1alpha1/cluster-name" = aws_eks_cluster.example.name
    "alpha.eksctl.io/iamserviceaccount-name"      = "kube-system/aws-load-balancer-controller"
    "alpha.eksctl.io/eksctl-version"              = "0.47.0"
  }
}

data "aws_iam_policy_document" "aws_loadbalancer_controller_assume_policy" {
  statement {
    actions = ["sts:AssumeRoleWithWebIdentity"]
    effect  = "Allow"

    condition {
      test     = "StringEquals"
      variable = "${replace(aws_iam_openid_connect_provider.for_eks_fargate_pod.url, "https://", "")}:sub"
      values   = ["system:serviceaccount:kube-system:aws-load-balancer-controller"]
    }

    condition {
      test     = "StringEquals"
      variable = "${replace(aws_iam_openid_connect_provider.for_eks_fargate_pod.url, "https://", "")}:aud"
      values   = ["sts.amazonaws.com"]
    }

    principals {
      identifiers = [aws_iam_openid_connect_provider.for_eks_fargate_pod.arn]
      type        = "Federated"
    }
  }
}

resource "aws_iam_role_policy" "aws_loadbalancer_controller" {
  name   = local.eksawsloadbalancercontroller_policy_name
  role   = aws_iam_role.aws_loadbalancer_controller.name
  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "iam:CreateServiceLinkedRole"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "iam:AWSServiceName": "elasticloadbalancing.amazonaws.com"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeAccountAttributes",
                "ec2:DescribeAddresses",
                "ec2:DescribeAvailabilityZones",
                "ec2:DescribeInternetGateways",
                "ec2:DescribeVpcs",
                "ec2:DescribeVpcPeeringConnections",
                "ec2:DescribeSubnets",
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeInstances",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DescribeTags",
                "ec2:GetCoipPoolUsage",
                "ec2:DescribeCoipPools",
                "elasticloadbalancing:DescribeLoadBalancers",
                "elasticloadbalancing:DescribeLoadBalancerAttributes",
                "elasticloadbalancing:DescribeListeners",
                "elasticloadbalancing:DescribeListenerCertificates",
                "elasticloadbalancing:DescribeSSLPolicies",
                "elasticloadbalancing:DescribeRules",
                "elasticloadbalancing:DescribeTargetGroups",
                "elasticloadbalancing:DescribeTargetGroupAttributes",
                "elasticloadbalancing:DescribeTargetHealth",
                "elasticloadbalancing:DescribeTags"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cognito-idp:DescribeUserPoolClient",
                "acm:ListCertificates",
                "acm:DescribeCertificate",
                "iam:ListServerCertificates",
                "iam:GetServerCertificate",
                "waf-regional:GetWebACL",
                "waf-regional:GetWebACLForResource",
                "waf-regional:AssociateWebACL",
                "waf-regional:DisassociateWebACL",
                "wafv2:GetWebACL",
                "wafv2:GetWebACLForResource",
                "wafv2:AssociateWebACL",
                "wafv2:DisassociateWebACL",
                "shield:GetSubscriptionState",
                "shield:DescribeProtection",
                "shield:CreateProtection",
                "shield:DeleteProtection"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:RevokeSecurityGroupIngress"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateSecurityGroup"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateTags"
            ],
            "Resource": "arn:aws:ec2:*:*:security-group/*",
            "Condition": {
                "StringEquals": {
                    "ec2:CreateAction": "CreateSecurityGroup"
                },
                "Null": {
                    "aws:RequestTag/elbv2.k8s.aws/cluster": "false"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateTags",
                "ec2:DeleteTags"
            ],
            "Resource": "arn:aws:ec2:*:*:security-group/*",
            "Condition": {
                "Null": {
                    "aws:RequestTag/elbv2.k8s.aws/cluster": "true",
                    "aws:ResourceTag/elbv2.k8s.aws/cluster": "false"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:RevokeSecurityGroupIngress",
                "ec2:DeleteSecurityGroup"
            ],
            "Resource": "*",
            "Condition": {
                "Null": {
                    "aws:ResourceTag/elbv2.k8s.aws/cluster": "false"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:CreateLoadBalancer",
                "elasticloadbalancing:CreateTargetGroup"
            ],
            "Resource": "*",
            "Condition": {
                "Null": {
                    "aws:RequestTag/elbv2.k8s.aws/cluster": "false"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:CreateListener",
                "elasticloadbalancing:DeleteListener",
                "elasticloadbalancing:CreateRule",
                "elasticloadbalancing:DeleteRule"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:AddTags",
                "elasticloadbalancing:RemoveTags"
            ],
            "Resource": [
                "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*",
                "arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*",
                "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*"
            ],
            "Condition": {
                "Null": {
                    "aws:RequestTag/elbv2.k8s.aws/cluster": "true",
                    "aws:ResourceTag/elbv2.k8s.aws/cluster": "false"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:AddTags",
                "elasticloadbalancing:RemoveTags"
            ],
            "Resource": [
                "arn:aws:elasticloadbalancing:*:*:listener/net/*/*/*",
                "arn:aws:elasticloadbalancing:*:*:listener/app/*/*/*",
                "arn:aws:elasticloadbalancing:*:*:listener-rule/net/*/*/*",
                "arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:AddTags"
            ],
            "Resource": [
                "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*",
                "arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*",
                "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*"
            ],
            "Condition": {
                "StringEquals": {
                    "elasticloadbalancing:CreateAction": [
                        "CreateTargetGroup",
                        "CreateLoadBalancer"
                    ]
                },
                "Null": {
                    "aws:RequestTag/elbv2.k8s.aws/cluster": "false"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:ModifyLoadBalancerAttributes",
                "elasticloadbalancing:SetIpAddressType",
                "elasticloadbalancing:SetSecurityGroups",
                "elasticloadbalancing:SetSubnets",
                "elasticloadbalancing:DeleteLoadBalancer",
                "elasticloadbalancing:ModifyTargetGroup",
                "elasticloadbalancing:ModifyTargetGroupAttributes",
                "elasticloadbalancing:DeleteTargetGroup"
            ],
            "Resource": "*",
            "Condition": {
                "Null": {
                    "aws:ResourceTag/elbv2.k8s.aws/cluster": "false"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:RegisterTargets",
                "elasticloadbalancing:DeregisterTargets"
            ],
            "Resource": "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:SetWebAcl",
                "elasticloadbalancing:ModifyListener",
                "elasticloadbalancing:AddListenerCertificates",
                "elasticloadbalancing:RemoveListenerCertificates",
                "elasticloadbalancing:ModifyRule"
            ],
            "Resource": "*"
        }
    ]
}
EOF
}

KubernetesのServiceAccountの設定

前節で作ったIAMロールをKubernetesのServiceAccountと連動させる。
eks.amazonaws.com/role-arnのannotationで紐づけを行う。
ALB Load Balancer Controllerを作成するNamespaceはサービス提供するNamespaceではなくkube-systemなので注意だ。

################################################################################
# Service Account                                                              #
################################################################################
resource "kubernetes_service_account" "awsloadbalancercontroller" {
  metadata {
    namespace = "kube-system"
    name      = "aws-load-balancer-controller"

    annotations = {
      "eks.amazonaws.com/role-arn" = aws_iam_role.aws_loadbalancer_controller.arn
    }
  }
}

HelmでAWS Load Balancer Controllerを起動する

AWS Load Balancer ControllerはHelmから作成するのが手っ取り早い。
というか、個別に定義するのはかなり大変(Manifestが500行程度ある)だ。
ServiceAccountは自動で作ることもできるが、今回は仕組みを知るためにもFalseにした。

################################################################################
# Helm(AWS Load Balancer Controller)                                           #
################################################################################
resource "helm_release" "aws_load_balancer_controller" {
  depends_on = [kubernetes_service_account.awsloadbalancercontroller]

  name       = "aws-load-balancer-controller"
  repository = "https://aws.github.io/eks-charts"
  chart      = "aws-load-balancer-controller"

  namespace = "kube-system"

  wait_for_jobs = true

  set {
    name  = "clusterName" // EKSのクラスタ名
    value = aws_eks_cluster.example.name
  }
  set {
    name  = "region" // EKSクラスタを起動しているリージョン
    value = data.aws_region.current.name
  }
  set {
    name  = "vpcId" // EKSクラスタを起動しているVPCのVPC-ID
    value = aws_vpc.for_eks_fargate.id
  }
  set {
    name  = "serviceAccount.create" // ServiceAccountを自動で作成するか
    value = false
  }
  set {
    name  = "serviceAccount.name" // 前節で作成したServiceAccountと合わせる
    value = "aws-load-balancer-controller"
  }
  set {
    name  = "ingressClassParams.create" // IngressClassを自動で作るか
    value = false
  }
  set {
    name  = "createIngressClassResource" // IngressClassを自動で作るか
    value = false
  }
}

ALBのターゲットグループに組み込む

ALBのターゲットグループに組み込むには、カスタムリソースであるTargetGroupBindingを作成する必要がある。これは、以下のCRDS(
Custom Resource Definitions)のManifestから作成することが可能だ。

$ kubectl apply -k "github.com/aws/eks-charts/stable/aws-load-balancer-controller/crds?ref=master"

さて、これで準備が整った。

今回は、NginxのDeployment, Serviceのリソース群とあわせて以下のManifestを作成してkubectl applyする。なお、ALB Load Balancer ControllerがIngressの役割を肩代わりをしてくれるので、↑の節で作成していたIngressはこの方法の場合は作成不要だ。

apiVersion: elbv2.k8s.aws/v1beta1
kind: TargetGroupBinding
metadata:
  name: 組み込みたいターゲットグループのターゲットグループ名
  namespace: NginxのDeployment, Serviceと同じNamespace
spec:
  serviceRef:
    name: NginxのServiceと同じ名前
    port: 80
  targetGroupARN: 組み込みたいターゲットグループのARN
  targetType: ip

これでPodを起動すると、自動でターゲットグループに組み込んでくれるようになる。

16
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
12