cloudwithbass

Terraform 연습하기 7: 'instances failed to join cluster' 트러블 슈팅 본문

Terraform

Terraform 연습하기 7: 'instances failed to join cluster' 트러블 슈팅

여영클 2024. 9. 5. 13:38

지난 포스팅까지 테라폼으로 구성했던 인프라의 구성도입니다. 

이번 포스팅에선 private subnet의 ec2 instance를 제거하고, eks nodegroup을 통해 private subnet의 인스턴스를 관리해보겠습니다.

또한 그 과정에서 발생한 'instances failed to join cluster' 에러를 해결한 과정을 기록했습니다.

 

아직 테라폼을 공부 중이기 때문에 내용이 부정확할 수 있지만.. 어쨌든 에러는 해결했습니다.

또한 블로그에서 다루지 못한 내용이 있을 수 있으며, 최신 상태의 전체 코드는 아래 github에서 확인하실 수 있습니다.

https://github.com/Dminus251/practice-terraform/tree/main/demo07-ng

 

목차


    1. Launch Template 구성하기

    node group을 구성하기에 앞서, node를 커스텀해서 생성하기 위해선 launch template 리소스가 필요합니다.

    launch template을 사용하면 노드의 ami, 보안 그룹, 인스턴스 유형 등을 지정할 수 있습니다.

    따라서 우선 Launch Template을 구성할 것입니다.


    modules/t-aws-launch_template/main.tf

    resource "aws_launch_template" "example" {
      image_id               = var.lt-image_id #AMI
      instance_type          = var.lt-instance_type
    
      vpc_security_group_ids = var.lt-sg
      #user_data             = base64encode("#!/bin/bash\n/etc/eks/bootstrap.sh ${var.cluster-name}\n")
      key_name               = var.lt-key_name
      user_data = base64encode(<<EOF
            #!/bin/bash
            /etc/eks/bootstrap.sh ${var.cluster-name}
            yum update -y &&
            curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.30.2/2024-07-12/bin/linux/amd64/kubectl &&
            chmod +x ./kubectl &&
            mv ./kubectl /usr/local/bin/ &&
            aws configure set aws_access_key_id ${var.aws_access_key_id} &&
            aws configure set aws_secret_access_key ${var.aws_access_key_secret} &&
            aws configure set region ${var.region} &&
            aws eks update-kubeconfig --region ${var.region} --name ${var.cluster-name}
            EOF
            )
    
    }

     

    노드의 ami, 인스턴스 유형, 보안 그룹을 지정합니다.

    user_data atrribute는 이 포스팅 4번 항목의 ERROR 3에서 설명하겠습니다.


    modules/t-aws-launch_template/vars.tf

    variable "lt-image_id" {
      type = string
      #default = "ami-05d2438ca66594916" #ubuntu 22.04
      #default = "ami-008d41dbe16db6778" #amazon linux 2023
      #default = "ami-04f3fb3944c844ddf" #eks optimized amazon linux 2023
      default = "ami-0e7ba98e45be88346" #eks optimized amazon linux 2
    }
    
    variable "lt-instance_type" {
      type = string
      default = "t3.medium"
    }
    
    variable "lt-sg" {
      type = list(string)
    }
    
    
    variable "lt-key_name"{
      type = string
    }
    
    ####user_data 내용
    variable "cluster-name"{
      type = string
    }
    
    variable "aws_access_key_id" {
      type = string
    }
    
    variable "aws_access_key_secret" {
      type = string
    }
    
    variable "region" {
      type = string
    }

    lt-image_id 변수는 주석 처리가 되지 않은 값을 사용합니다.

    처음엔 저에게 익숙한 ubuntu 22.04를 사용했었는데, eks optimized amazon linux2 AMI를 사용해야 합니다.

    이에 관해선 트러블 슈팅 항목에서 추가로 설명하겠습니다.


    modules/t-aws-launch_template/outputs.tf  

    output "lt_id" {
      value = aws_launch_template.example.id
    }

    노드 그룹에서 launch template의 id를 사용하기 위해 output으로 내보냅니다.


    2. Node Group 구성하기

    eks 클러스터와 마찬가지로, 노드 그룹 또한 노드 그룹의 role이 필요합니다.

    따라서 2-1에서 Node Group의 Role을, 2-2에서 Node Group을 구성하겠습니다.

     


    2-1. Node Group Role

    modules/t-aws-eks/role/ng_role/main.tf

    코드는 테라폼 문서의 example IAM Role for EKS Node Group을 사용했습니다.

    resource "aws_iam_role" "example" {
      name = "eks-node-group-example"
    
      assume_role_policy = jsonencode({
        Statement = [{
          Action = "sts:AssumeRole"
          Effect = "Allow"
          Principal = {
            Service = "ec2.amazonaws.com"
          }
        }]
        Version = "2012-10-17"
      })
    }
    
    resource "aws_iam_role_policy_attachment" "example-AmazonEKSWorkerNodePolicy" {
      policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
      role       = aws_iam_role.example.name
    }
    
    resource "aws_iam_role_policy_attachment" "example-AmazonEKS_CNI_Policy" {
      policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
      role       = aws_iam_role.example.name
    }
    
    resource "aws_iam_role_policy_attachment" "example-AmazonEC2ContainerRegistryReadOnly" {
      policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
      role       = aws_iam_role.example.name
    }
    • AmazonEKSWorkerNodePolicy: 워커 노드가 EKS Cluster와 연결할 수 있도록 허용합니다.
    • AmazonEKS_CNI_Policy: Amazon VPC 플러그인에 워커 노드의 IP를 수정할 수 있는 권한입니다.
    • AmazonEC2ContainerRegistryReadOnly: EC2 Container Registry를 읽을 수 있는 권한입니다. 

    assume_role_policy 대한 자세한 설명은 이전 포스팅의 '2-1. Assume Role이란?' 항목을 참고해주세요.


    modules/t-aws-eks/role/ng_role/outputs.tf

    output "arn" {
      value = aws_iam_role.example.arn
    }

    노드 그룹이 이 role을 사용하기 위해 arn을 output으로 내보냅니다.

    이제 노드 그룹의 role을 구성을 마쳤으니, 다시 node group을 구성해보겠습니다.


    2-2 Node Group

    modules/t-aws-eks/ng/main.tf

    resource "aws_eks_node_group" "example" {
      cluster_name    = var.cluster-name
      node_group_name = var.ng-name
      node_role_arn   = var.ng-role_arn
      subnet_ids      = var.subnet-id
    
      scaling_config {
        desired_size = 1
        max_size     = 2
        min_size     = 1
      }
      launch_template {
        id      = var.ng-lt_id
        version = "$Latest"
      }
    }

    클러스터 이름, 노드 그룹 이름, 노드 그룹의 role arn, subnet id, 스케일링 정보와 launch template을 설정합니다.

    launch template의 버전은 하나밖에 없으니 version을 $Latest로 설정합니다.

    이 항목에서 테라폼은 $Latest와 $Default를 허용하며, 자동으로 그에 상응하는 number로 변환해줍니다.


    modules/t-aws-eks/ng/vars.tf

    variable "cluster-name"{
      type = string
    }
    
    variable "ng-name"{
      type = string
    }
    
    variable "ng-role_arn"{
      type = string
    }
    
    
    variable "subnet-id"{
      type = list(string)
    }
    
    variable "ng-lt_id"{
      description = "id of launch template for node group"
      type = string
    }

     


    3. 루트 모듈 구성하기

    #role of node_group
    module "ng-role"{
      source = "./modules/t-aws-eks/role/ng_role"
    }
    
    #launch template for node_group
    module "lt-ng"{
      source        = "./modules/t-aws-launch_template"
      lt-sg         = [module.sg-node_group.sg-id]
      lt-key_name   = data.aws_key_pair.example.key_name
      cluster-name  = module.eks-cluster.cluster-name
      aws_access_key_id = var.AWS_ACCESS_KEY
      aws_access_key_secret = var.AWS_SECRET_KEY
      region = var.AWS_REGION
    }
    
    module "node_group"{
      source           = "./modules/t-aws-eks/ng"
      for_each         = {for i, subnet in module.private_subnet: i => subnet}
      cluster-name     = module.eks-cluster.cluster-name
      ng-name          = "pracite-ng-${each.key}"
      ng-role_arn      = module.ng-role.arn
      subnet-id        =  [each.value["private_subnet-id"]]
      ng-lt_id         = module.lt-ng.lt_id
      depends_on       = [module.eks-cluster, module.ng-role]
    }

    이제 각 항목에서 구성한 모듈을 루트 모듈에서 구성해 줍니다.

    lt-ng 모듈의 변수 lt-sg는 트러블 슈팅 과정에서 추가한 변수이니 일단은 무시해주세요.

    node_group 모듈에서는, private subnet마다 노드 그룹을 구성합니다.

     


    4. 트러블 슈팅

    위 코드들을 바탕으로 terraform apply를 실행했더니,  NodeCreationFailure: Instances failed to join the kubernetes cluster 에러가 발생했습니다.

     

    그리고 아주 감사하게도 AWS에 Worker Node의 트러블 슈팅 문서가 있었습니다.

    문서에 따라 AWS Systems manager -> Automation에 들어갑니다.

     

     

    우측의 Execute automation 버튼을 누르, 좌측 Documentatiton categories에서 others를 체크합니다.

    이후 검색창에  WorkerNode를 검색하면 결과가 하나 나올 것입니다.

    그 결과의 이름을 누르면 외부 문서로 이동합니다.

     

    외부 문서로 이동했으면, 아래 사진에서 자동화 실행 버튼을 클릭합니다.

     

    이제 이 곳에서 Input paramters에 Worker Node 인스턴스 id와 Cluster Name을 입력하고 Execute 버튼을 누르면 자동으로 어떤 에러가 발생했는지 알려줍니다.

     

    아래는 제 경우의 에러들입니다.

     

    정말 많은데요.. 에러를 하나씩 해결해보겠습니다.

    [ERROR]: The cluster Security Group sg-0b21fb8d41f325c6b is not allowing traffic from the worker node.
    [ERROR]: Cluster VPC vpc-07449f9662aa28a13 must have "enableDnsHostnames" and "enableDnsSupport" set to true, current attributes: {'DNS_Hostname': False, 'DNS_Support': True}. Please review this URL for further details: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-dns.html#vpc-dns-updating
    [ERROR]: The UserData of the worker node must contain the bootstrap script with correct EKS cluster name. Please review this URL for further details: https://aws.amazon.com/premiumsupport/knowledge-center/eks-worker-nodes-cluster/
    [WARNING]: Worker node's AMI ami-05d2438ca66594916 differs from the public EKS Optimized AMIs. Ensure that the Kubelet daemon is at the same version as your cluster's version 1.30 or only one minor version behind. Please review this URL for further details: https://kubernetes.io/releases/version-skew-policy/ .
    [WARNING]: No secondary private IP addresses are assigned to worker node i-052d892b47de00cab, ensure that the CNI plugin is running properly. Please review this URL for further details: https://docs.aws.amazon.com/eks/latest/userguide/pod-networking.html
    [WARNING]: As SSM agent is not reachable on worker node, this document did not check the status of Containerd, Docker and Kubelet daemons. Ensure that required daemons (containerd, docker, kubelet) are running on the worker node using command "systemctl status <daemon-name>".

     

    1번 에러를 해결하려면 많은 내용이 필요하므로 때문에 2번과 3번 에러부터 해결 방법을 찾아보겠습니다.


    ERROR 2: Cluster VPC must have set "enableDnsHostnames" True

    현재 DNS_hostname이 False 상태이므로 단순하게 True로 바꾸면 되겠습니다.

    테라폼의 VPC 리소스 문서를 확인하니, attribute 중 enable_dns_hostnames attribute로 변경할 수 있었습니다.

     

    modules/t-aws-vpc/main.tf

    resource "aws_vpc" "main" {
      cidr_block       = var.vpc-cidr
      instance_tenancy = "default"
      enable_dns_hostnames = true
      tags = {
        Name = var.vpc-name
      }
    }

    DNS Hostname은 VPC 내에서 IP 대신 DNS를 통해 인스턴스에 접근할 수 있는 기능입니다.

     

    Kubernetes의 DNS for Service and Pos 문서에 따르면 쿠버네티스 워크로드는, IP 주소가 아닌 DNS를 이용해 Service를 찾는다고 합니다.

    조금 더 자세히 들어가면, 데이터 플레인의 요소 중 kubelet이 DNS를 구성해서, IP가 아닌 DNS를 이용해 컨테이너가 서비스를 찾을 수 있게 한다고 합니다.

     

    이런 이유로 enable_dns_hostnames 속성을 true로 설정해야 한다고 추측해 봅니다.

     


    ERROR 3:  The UserData of the worker node must contain the bootstrap script with correct EKS cluster name. 

     

    AWS의 문서 'How can I get my worker nodes to join my Amazon EKS cluster?'를 보면 다음과 같은 내용이 있습니다.

     

    이 포스팅의 1번 항목인 modules/t-aws-launch_template/main.tf 에서 확인했던 userdata가 이 내용입니다.

    resource "aws_launch_template" "example" {
      #other configurations..
      
      user_data = base64encode(<<EOF
            #!/bin/bash
            /etc/eks/bootstrap.sh ${var.cluster-name}
            yum update -y &&
            curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.30.2/2024-07-12/bin/linux/amd64/kubectl &&
            chmod +x ./kubectl &&
            mv ./kubectl /usr/local/bin/ &&
            aws configure set aws_access_key_id ${var.aws_access_key_id} &&
            aws configure set aws_secret_access_key ${var.aws_access_key_secret} &&
            aws configure set region ${var.region} &&
            aws eks update-kubeconfig --region ${var.region} --name ${var.cluster-name}
            EOF
            )
    }

    테라폼은 base64로 인코딩해서 데이터를 전달하기 때문에 base64encode()함수를 사용해줍니다.

     

    /etc/eks/bootstrap.sh ${var.cluster-name} 스크립트는 워커 노드가 어떤 EKS 클러스터에 연결될지를 결정하며, 워커 노드가 클러스터의 마스터 노드와 통신할 수 있도록 설정합니다.

     

    그 후 yum update로 패키지를 업데이트하고, 쿠버네티스 1.30버을 설치합니다. 그 후 클러스터에 접근하기 위해 aws configure 설정을 하고, kubeconfig를 업데이트합니다.

     

    여기서 주의할 점은 워커 노드에 ssh 연결을 하고, /var/log/cloud-init-output.log 파일을 보면 다음과 같은 내용이 있습니다.

    config 파일은 root 유저의 홈 디렉터리에 생성됩니다.

    kubectl 명령은 $HOME/.kube/config를 참조하므로(출처)

    root 유저가 아니면 아래 사진처럼 클러스터에 접근할 수 없습니다.


    ERROR 1: The cluster Security Group is not allowing traffic from the worker node.

    클러스터의 보안 그룹이, 노드 그룹의 보안 그룹을 허용하지 않습니다.

    Kubernetes Component 문서나, The Kubernetes API 문서에서 HTTP API를 사용한다고 합니다.

     

    HTTP protocol과 HTTP API는 다르기에 80번 포트를 열면 안 됩니다. 

     

    Controlliing Access to the Kubernetes API 문서를 확인하면, Kubernetes API server는 TLS를 이용하며, 443 포트에서 서비스됨을 알 수 있습니다.

     

    실제로 443 포트가 아닌, 80 포트만을 허용하면 아래처럼 또 'instances failed to join cluster'에러가 발생합니다.

    (이번 포스팅을 하면서 수십 차례 노드 그룹을 생성해본 결과, 정상적인 경우 4분 이내 노드 그룹 프로비저닝이 완료되고, 그이상 넘어가면 20분대에서 instances failed to join cluster 에러가 발생하며 terraform apply에 실패합니다.)

     

    따라서 루트 모듈에 다음과 같이 보안 그룹과  보안 그룹 규칙을 추가해줍니다.

    이 과정에서 기존 sg 모듈로 aws_security_group 리소스 내에서 ingress를 정의했지만, aws_security_group_rule리소스를 사용하도록 수정했습니다. aws_security_group_rule 모듈의 코드는 git을 참고해주세요. https://github.com/Dminus251/practice-terraform/tree/main/demo07-ng/modules/t-aws-sg_rule


    main.tf

    코드가 길지만 내용은 간단합니다.

    그 내용은 코드의 아래에서 요약하겠습니다.

    module "sg-cluster" {
      source     = "./modules/t-aws-sg"
      sg-vpc_id  = module.vpc.vpc-id
      sg-name    = "sg_cluster"
    }
    
    module "sg-node_group" {
      source     = "./modules/t-aws-sg"
      sg-vpc_id  = module.vpc.vpc-id
      sg-name    = "sg_nodegroup"
    }
    
    module "sg_rule-cluster" {
      source = "./modules/t-aws-sg_rule-sg"
      sg_rule-type = "ingress"
      sg_rule-from_port = 443
      sg_rule-to_port = 443
      sg_rule-protocol = "tcp"
      sg_rule-sg_id = module.sg-cluster.sg-id #규칙을 적용할 sg
      sg_rule-source_sg_id = module.sg-node_group.sg-id #허용할 sg
    }
    
    
    #노드 그룹의 sg에서 클러스터 sg의 ingress traffic 허용
    module "sg_rule-ng" {
      source = "./modules/t-aws-sg_rule-sg"
      sg_rule-type = "ingress"
      sg_rule-from_port = 443
      sg_rule-to_port = 443
      sg_rule-protocol = "tcp"
      sg_rule-sg_id = module.sg-node_group.sg-id #규칙을 적용할 sg
      sg_rule-source_sg_id = module.sg-cluster.sg-id #허용할 sg
    }
    • 클러스터의 보안 그룹의 규칙
      • 인바운드: 노드 그룹의 보안 그룹에서 온 443 포트 트래픽을 허용
      • 아웃바운드: 모든 트래픽 허용
    • 노드 그룹의 보안 그룹의 규칙
      • 인바운드: 클러스터의 보안 그룹에서 온 443 포트 트래픽을 허용
      • 아웃바운드: 모든 트래픽 허용

    위와 같이 보안 그룹을 추가했지만 여전히 'instances failed to join cluster'' 에러가 발생합니다.


    클러스터 보안  그룹 vs 추가 보안 그룹

    에러가 지속된 원인은 이것입니다.

    클러스터의 보안 그룹은 '클러스터 보안 그룹'과 '추가 보안 그룹'으로 나뉩니다.

    위 코드에서 작성한 sg-cluster 모듈은, terraform apply 결과, 추가 보안 그룹에 적용되는 것을 확인했습니다.

     

    이후 노드 그룹의 보안 그룹 트래픽을 허용하는 rule을 다음과 같이 세 가지 케이스로 나누어 적용해봤는데요

    1. 클러스터 보안 그룹에만 적용
    2. 추가 보안 그룹에만 적용 (현재 코드)
    3. 클러스터 보안 그룹과 추가 보안 그룹 모두 적용

    그 결과 2번을 제외한 모든 경우에서 노드 그룹 생성에 성공했고, 이를 미루어 보아 '클러스터 보안 그룹'에 규칙을 추가해야 한다는 것을 알 수 있습니다.

     

    AWS에선 클러스터 보안 그룹을 다음과 같이 설명합니다.

    클러스터 보안 그룹은 Kubernetes 제어 플레인과 클러스터의 컴퓨팅 리소스 간의 통신을 제어하는 데 사용되는 통합 보안 그룹입니다. 클러스터 보안 그룹은 기본적으로 Amazon EKS에서 관리하는 Kubernetes 제어 플레인과 Amazon EKS API를 통해 생성된 모든 관리형 컴퓨팅 리소스에 적용합니다.

    노드 그룹은 클러스터의 컴퓨팅 리소스에 속하므로 클러스터 보안 그룹에 규칙을 추가해야 합니다.

     

    아무튼 , 테라폼의 eks_cluster 리소스 문서를 확인하면, attribute reference 중에 cluster_security_group_id가 있습니다. 이 속성을 이용할 것입니다.

     

    modules/t-aws-eks/cluster/outputs.tf

    output "cluster-sg" {
      value = aws_eks_cluster.example.vpc_config[0].cluster_security_group_id
    }

    vpc_config는 리스트 형식으로 제공되기 때문에 [0] 인덱스로 참조해서 clsuter sg의 id를 output으로 내보냅니다.

    VPC_CONFIG를 Output으로 출력해본 결과


    main.tf

    #클러스터 메인 보안 그룹
    module "sg_rule-main_cluster" {
      source = "./modules/t-aws-sg_rule-sg"
      sg_rule-type = "ingress"
      sg_rule-from_port = 443
      sg_rule-to_port = 443
      sg_rule-protocol = "tcp"
      sg_rule-sg_id = module.eks-cluster.cluster-sg #규칙을 적용할 sg
      sg_rule-source_sg_id = module.sg-node_group.sg-id #허용할 sg
      depends_on = [module.eks-cluster]
    }

    이제 루트 모듈의 main.tf에 새로운 security group rule을 추가합니다.

    sg_rule-sg_id 변수에 방금 작성했던 output인 클러스터 보안 그룹의 id를 사용하고, 허용할 sg에는 노드 그룹의 sg-id를 사용하면 됩니다.

     

    이제 ERROR 1이 해결됐습니다.

     

    09.08 추가

    https://kubernetes.io/docs/reference/networking/ports-and-protocols/

     

    Ports and Protocols

    When running Kubernetes in an environment with strict network boundaries, such as on-premises datacenter with physical network firewalls or Virtual Networks in Public Cloud, it is useful to be aware of the ports and protocols used by Kubernetes components.

    kubernetes.io

     


    WARNING: Worker nodes' AMI differs from the public EKS Optimized AMIs.

    이제 ERROR가 해결됐음에도 여전히 'instances failed to join cluster'에러가 발생합니다.

    따라서 런북의 WARNING 메시지들까지 해결해보겠습니다.

     

    저는 처음에 노드 그룹의 launch template의 AMI에, 제게 익숙한 ubuntu 22.04를 사용했습니다.

    하지만 AWS의 최적화된 Amazon Linux AMI를 사용한 노드 생성 문서에서는 다음과 같이 설명합니다.

     

     

    Amazon Linux2와 Amazon Linux 2023을 사용해야 한다고 하는데, 무슨 이유인지 EKS Optimized Amazon Linux 2023의 AMI를 사용하면 같은 에러가 발생했습니다.

     

    따라서 Amazon Linux2를 사용해보겠습니다.

     

    다음 명령을 실행하면 eks에 최적화된 AMI의 정보를 볼 수 있습니다.

    aws ssm get-parameters --names "/aws/service/eks/optimized-ami/1.30/amazon-linux-2/recommended"

     

     

    이 결과로 나온 ami-0e7ba98e45be88346를 콘솔에서 확인해 보면 EKS 워커 노드에 최적화된 Amazon Linux2 이미지임을 알 수 있습니다.

     

    사실 위 명령은 GPT에게 물어본 명령이고요.. AWS의 EKS 권장 ami 검색 문서가 있긴 하던데 이 문서의 코드를 입력하면 아무런 결과가 나오지 않습니다.

    aws ssm get-parameter --name /aws/service/eks/optimized-ami/1.30/amazon-linux-2/x86_64/standard/recommended/image_id     --region ap-northeast-2 --query "Parameter.Value" --output text
    
    #Output: An error occurred (ParameterNotFound) when calling the GetParameter operation:

     

    아무튼, launch teamplte의 AMI를 변경해줍니다. 


     

    modules/t-aws-launch_template/vars.tf

    #Other variables...
    
    variable "lt-image_id" {
      type = string
      #default = "ami-05d2438ca66594916" #ubuntu 22.04
      #default = "ami-008d41dbe16db6778" #amazon linux 2023
      #default = "ami-04f3fb3944c844ddf" #eks optimized amazon linux 2023
      default = "ami-0e7ba98e45be88346" #eks optimized amazon linux 2
    }

     

    드디어 instances failed to join cluster 에러가 발생하지 않고, 프로비저닝에 성공합니다.