Open6

TerraformでEKSを構築する

公式モジュールを利用してネットワークおよびEKSクラスタを構築する。
ネットワークには VPCモジュール、EKSクラスタにはEKSモジュールを利用する。
基本的な設定はEKSモジュールのドキュメントに従い以下の通り。

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "2.78.0"

  name = "hackday"
  cidr = "10.1.0.0/16"
  azs  = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
  public_subnets  = ["10.1.0.0/24", "10.1.1.0/24", "10.1.2.0/24"]
}

data "aws_eks_cluster" "cluster" {
  name = module.my-cluster.cluster_id
}

data "aws_eks_cluster_auth" "cluster" {
  name = module.my-cluster.cluster_id
}

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

module "my-cluster" {
  source          = "terraform-aws-modules/eks/aws"
  cluster_name    = "hackday-cluster"
  cluster_version = "1.17"
  subnets         = module.vpc.public_subnets
  vpc_id          = module.vpc.vpc_id

  worker_groups = [
    {
      instance_type = "m4.large"
      asg_max_size  = 5
    }
  ]
}

これでEKSクラスタが構築できる。
eksctlやkubectlでクラスタの参照が可能。

2021-05-29 12:05:15 []  eksctl version 0.51.0
2021-05-29 12:05:15 []  using region ap-northeast-1
NAME		REGION		EKSCTL CREATED
hackday-cluster	ap-northeast-1	False

$ aws eks update-kubeconfig --name hackday-cluster
Added new context arn:aws:eks:ap-northeast-1:xxxxxxxxxxxx:cluster/hackday-cluster to /home/thaim/.kube/config

$ kubectl get node
NAME                                            STATUS   ROLES    AGE     VERSION
ip-10-1-2-194.ap-northeast-1.compute.internal   Ready    <none>   5m22s   v1.17.12-eks-7684af

サンプルとなるアプリをデプロイしてやる。

$ cat nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  selector:
    matchLabels:
      app: frontend
  replicas: 2
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: frontend
        image: nginx:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  type: LoadBalancer
  selector:
    app: frontend
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

$ kubectl get service
NAME         TYPE           CLUSTER-IP       EXTERNAL-IP                                                                   PORT(S)        AGE
frontend     LoadBalancer   172.20.127.205   ac03342f0e9c54c66a13ee43fcc5911f-927961018.ap-northeast-1.elb.amazonaws.com   80:30653/TCP   18s
kubernetes   ClusterIP      172.20.0.1       <none>   

あとはType: LoadBalancerの EXTERNAL-IPに記載されたドメインにアクセスすれば
見慣れたnginxの画面が確認できる。
(アクセスできるようになるまで1-2分掛かる)

EKSが利用するネットワークについて。
当初VPCモジュールを利用せずにネットワークを構築してEKSクラスタを構築しようとしたところ、
ワーカーノードが上手く動作してくれなかった。
クラスタは生成するがノードが検出できない。

$ eksctl get cluster
2021-05-29 13:26:28 []  eksctl version 0.51.0
2021-05-29 13:26:28 []  using region ap-northeast-1
NAME		REGION		EKSCTL CREATED
hackday-cluster	ap-northeast-1	False

$ kubectl cluster-info 
Kubernetes master is running at https://293189AABD2C71C06A5E4B7C758F82B1.gr7.ap-northeast-1.eks.amazonaws.com
CoreDNS is running at https://293189AABD2C71C06A5E4B7C758F82B1.gr7.ap-northeast-1.eks.amazonaws.com/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

$ kubectl get service
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   172.20.0.1   <none>        443/TCP   5m58s

$ kubectl get node
No resources found in default namespace.

原因は FAQ: Why are nodes not being registered? にある通り、
ワーカーノードがEKSクラスタのエンドポイントに接続できなかったから。

パブリックサブネットにあるノードなら public_ip = true にしろ、
プライベートサブネットにあるノードなら NATゲートウェイを有効にしろ、とある。
VPCモジュール経由でパブリックサブネットを構築する場合、
デフォルトでパブリックIPを付与してくれる
このため、EKSモジュールの説明にある通り public_ip = true にする必要はない。

もし VPCモジュールで map_public_ip_on_launch = false にした場合はやはりワーカーノードが動作しなくなる。
この場合は EKSモジュールで public_ip = true にすればワーカーノードが動作するようになる。

module "vpc" {
  ...

  map_public_ip_on_launch = false
}

module "my-cluster" {
  ...

  worker_groups = [
    {
      instance_type = "m4.large"
      asg_max_size  = 5
      public_ip     = true
    }
  ]
}

パブリックサブネットの利用とプライベートサブネットの利用について。
AWSブログ: Amazon EKS ワーカーノードの謎を解くクラスターネットワーク では、
EKSクラスタのVPCサブネットについて以下のような説明がある。

  1. パブリックサブネットのみを使用します。ノードとイングレスリソース (ロードバランサーなど) は、すべて同じパブリックサブネットでインスタンスを作成します。
  2. パブリックサブネットとプライベートサブネットを使用します。ノードはプライベートサブネットで、イングレスリソース (ロードバランサーなど) はパブリックサブネットでインスタンスを作成します。
  3. プライベートサブネットのみを使用します。ノードはプライベートサブネットでインスタンスを作成します。この設定は、パブリックインターネットからの通信を受信する必要がないワークロードにのみ使用されるため、パブリックイングレスリソースはありません。

EKSドキュメント: Creating a VPC for your Amazon EKS clusterにも同様の内容の記載がある。

しかし、実際にパブリックサブネット・プライベートサブネット混合のEKSクラスタを構築してみても
ワーカーノードはパブリックサブネットを利用していた。
terraformの設定が悪いのかと、eksctlでクラスタを作成してみたが
こちらでもワーカーノードにパブリックサブネットを利用していた。

このため、パブリックサブネットとプライベートサブネットの双方を利用するEKSクラスタにおいて
ワーカーノードがプライベートサブネットを利用するには別の取組みが必要と判断。
(または不具合orアップデート等により機能しなくなった)。
ここではこの検証は保留する。

EKSクラスタがサブネットリソースに付与するタグ情報について。
EKSクラスタを構築すると、サブネットグループのタグにを付与する。
これはEKSドキュメント Subnet tagging - Cluster VPC considerations にも記載がある。

terraform applyでVPCおよびEKSクラスタを構築直後にもう1度 terraform plan を実行すると
以下のような差分が発生する(全サブネットで同じdiffが発生するのでprivate[1/2] および public[1/2]は省略)

  # module.vpc.aws_subnet.private[0] will be updated in-place
  ~ resource "aws_subnet" "private" {
        id                              = "subnet-014534b4a841af1c0"
      ~ tags                            = {
          - "kubernetes.io/cluster/hackday-cluster" = "shared" -> null
            # (1 unchanged element hidden)
        }
      ~ tags_all                        = {
          - "kubernetes.io/cluster/hackday-cluster" = "shared" -> null
            # (1 unchanged element hidden)
        }
        # (9 unchanged attributes hidden)
    }

  # module.vpc.aws_subnet.public[0] will be updated in-place
  ~ resource "aws_subnet" "public" {
        id                              = "subnet-0e094fbdfcfc7146a"
      ~ tags                            = {
          - "kubernetes.io/cluster/hackday-cluster" = "shared" -> null
            # (1 unchanged element hidden)
        }
      ~ tags_all                        = {
          - "kubernetes.io/cluster/hackday-cluster" = "shared" -> null
            # (1 unchanged element hidden)
        }
        # (9 unchanged attributes hidden)
    }

どうやらEKSはこのタグ情報を利用してリソースのデプロイ先を決定するらしい。
このため、このタグを削除すると type:LoadBalancerのデプロイに失敗する らしい

これを解決する方法は3通りで、

  • ignore_changes に追加する
  • 最初からタグを埋め込む
  • EKS 1.19にアップデートする

ignore_changesの追加について、VPCモジュールを使わずにネットワークを構築している場合は容易だが、
VPCモジュールを利用している場合、サブネットのタグにignore_changesを追加することはできない。
このためこの方法は不適切。

よくある解決策としては、VPC構築時にEKSタグを埋め込む方法。
EKSモジュールのサンプルにもこの方法が採用されている。

module "vpc" {
  ...
  public_subnet_tags = {
    "kubernetes.io/cluster/${local.cluster_name}" = "shared"
    "kubernetes.io/role/elb"                      = "1"
  }

  private_subnet_tags = {
    "kubernetes.io/cluster/${local.cluster_name}" = "shared"
    "kubernetes.io/role/internal-elb"             = "1"
  }
}

もっと簡単なのがEKSクラスタのバージョンを1.19以上にアップデートする方法。
1.19よりVPCサブネットにタグを追加する挙動が廃止された。
このため、単に1.19を利用すればよい。
EKSモジュールではREADMEに記載は1.17のままだが、1.19も問題なく動作する。

プライベートサブネット用に構築するNAT Gatewayについて。
VPCモジュールはデフォルトではプライベートサブネット毎にNAT Gatewayを構築する。
これは可用性を考慮すると重要だが、実際には過剰なので構築するNAT Gatewayを制御したい。
実際eksctlで構築するクラスタもデフォルトでは1つのNAT Gatewayしか構築しない。
(eksctlの挙動はどこかにドキュメント化されていたり制御方法が記載されている?)

eksctlと同様に1つのNAT Gatewayしか構築しない場合は
single_nat_gateway = true を有効にすればよい。
これにより1つのNAT Gatewayを全プライベートサブネットで共有してくれる。

module "vpc" {
  ...
  single_nat_gateway = true
}

可用性も考慮したい場合は、AZ毎にNAT Gatewayを構築する
one_nat_gateway_per_az = true を有効にする方法もある。
ただし、1つのAZに複数のプライベートサブネットを構築する必要性はあまりないので、
これはデフォルトの挙動であるプライベートサブネット毎にNAT Gatewayを構築するのと
(少なくともEKSクラスタを構築する上では)ほぼ同じ挙動になる。

module "vpc" {
  ...
  one_nat_gateway_per_az = true
}

可用性も考慮して2つのNAT Gatewayを構築したい、みたいなケースはVPCモジュールでは対応できない。
自前で全ネットワークリソースを作成する必要がある。

Warningへの対応。
上記実装でのplanはすべて以下のような警告が出力される。

Warning: Version constraints inside provider configuration blocks are deprecated

原因はkubernetes providerの宣言内部にてバージョンを指定していること。
terraform 0.14より、このような指定方法は非推奨となった
(0.13から変更になって警告が出るようになったのが0.14から?)

provider "kubernetes" {
  ...
  version                = "~> 1.9"
}

今後は required_providersにて指定する必要がある。

terraform {
  required_providers {
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 1.9"
    }
  }
}

ちなみに、ここで利用しているkubernetesプロバイダはhashicorp namespace管理のものなのでsourceの指定は省略できる。
ただし、これはterraform 0.13より古いバージョンとの後方互換性のためのものなので、
明示的に記載することを推奨している。
ちなみにsourceアドレスでホスト名が公式terraformレジストリの場合は省略でき、
ホスト名の省略はデフォルトなので推奨されていとのこと。
以上から、ソースアドレスにはホスト名は省略してネームスペースとプロバイダ名のみ明示的に記載するのが推奨の書き方となる。

作成者以外のコメントは許可されていません