본문 바로가기
School/TFAS

[TFAS] 챕터 16 쿠버네티스 관련 프로바이더

by 세똔구리 SEDDONGURI 2025. 12. 14.
TFAS (TerraForm Addicted School)의 내용을 정리한 글입니다.
심각한 테라폼 중독입니다 - 책으로 스터디를 진행합니다.


현대 인프라 관리의 표준인 쿠버네티스에 대해서 쿠버네티스와 헬름 프로바이더를 알아보자.

쿠버네티스 프로바이더

쿠버네티스 프로바이더를 사용하면 테라폼을 통해 지정한 쿠버네티스 클러스터 내부에 쿠버네티스 리소스에 대한 관리 작업을 수행할 수 있다.

테라폼을 사용하지 않아도 헬름을 사용하면 쿠버네티스 객체를 선언적으로 관리할 수 있다. 일반적으로 현대의 애플리케이션을 배포하는 경우 헬름 차트 혹은 ArgoCD를 사용한 깃옵스 기반의 배포도구를 통해 헬름 차트를 설치하기 때문에 테라폼을 사용할 이유가 많지 않다.

그러나 퍼블릭 클라우드를 사용한다면 클러스터를 맨 처음으로 프로비저닝할 때는 테라폼을 사용하여 프로비저닝을 진행할 수 있다.

 

쿠버네티스 프로바이더를 사용하여 EKS 클러스터 접근

쿠버네티스 리소스를 사용하기 전 EKS 클러스터를 생성했을 때 클러스터에 접근할 수 있도록 쿠버네티스 프로바이더를 사용하는 법을 먼저 알아보자. EKS 클러스터를 테라폼으로 사용한다면, 생성된 EKS 클러스터에 추가 설정 없이 곧바로 접근할 수 있도록 테라폼 쿠버네티스 프로바이더를 사용하는 것이 좋다.

resource "aws_eks_cluster" "this" {
  name = "tf-eks"
  # ...
}

data "aws_eks_cluster_auth" "this" {
  name = aws_eks_cluster.this.id
}


locals {
  cluster_token          = data.aws_eks_cluster_auth.this.token
  cluster_ca_certificate = base64decode(aws_eks_cluster.this.certificate_authority[0].data)
  cluster_endpoint       = aws_eks_cluster.this.endpoint
}


provider "kubernetes" {
  host                   = local.cluster_endpoint
  token                  = local.cluster_token
  cluster_ca_certificate = local.cluster_ca_certificate
}

여기서 주의해야할 점은 실행환경의 IP에서 클러스터에서 접근가능해야 한다. 클러스터의 보안그룹에 실행 환경의 IP 주소를 소스로 443 포트로 요청을 허용하는 규칙이 존재해야 한다. public access를 true로 설정한다면 0.0.0.0/0에 대한 규칙이 포함되나, private access 만 허용되는 경우 추가 클러스터 보안그룹을 생성하여 ip에 대한 인바운드 규칙을 설정해야 한다.

 

프라이빗 통신만 허용하는 추가 클러스터 보안 그룹을 생성해야 한다.

resource "aws_security_group" "this" {
  name = "tf-eks-sg"
}


locals {
  my_sg_id = aws_security_group.this.id
  my_cidr  = "10.0.0.0/16"
}

resource "aws_security_group_rule" "this" {
  security_group_id = local.my_sg_id
  from_port         = 443
  to_port           = 443
  protocol          = "tcp"
  type              = "ingress"
  cidr_blocks       = [local.my_cidr]
}

resource "aws_eks_cluster" "this" {
  vpc_config {
    endpoint_private_access = true
    endpoint_public_access  = false
    security_group_ids      = [local.my_sg_id]
    subnet_ids              = ["", ""]
  }
  name       = "tf-eks-cluster"
  role_arn   = ""
  depends_on = [aws_security_group.this]
}

쿠버네티스 프로바이더는 aws_eks_cluster 리소스의 출력값을 사용해 설정하기때문에 프로바이더와 클러스터 리소스는 의존관계가 된다. 그러나 실행 환경에서 프라이빗 클러스터에 접속하기 위해서는 추가 보안 그룹의 규칙인 aws_security_group_rule 리소스가 프로바이더 설정까지 전달되어야 한다.

이런 의존관계를 구현하려면 depends_on 을 활용해 명시적 의존 관계를 설정해야 하지만, 프로바이더 블록에서는 depends_on 인수를 사용할 수 없다. 따라서 프로바이더가 암시적으로 의존하고 있는 aws_eks_cluster 리소스에 명시적으로 aws_security_group_rule 리소스에 대한 의존성을 설정한다.

이렇게 하면 쿠버네티스 프로바이더를 사용하기 전에 실행 환경에서 쿠버네티스에 직접 연결하기 위한 의존 관계가 올바르게 설정된다.

 

기본 스토리지 클래스 재설정

EKS 클러스터를 생성하면 기본적으로 gp2 타입의 EBS 스토리지 클래스가 생성된다. 그리고 해당 스토리지 클래스가 기본 클래스로 설정된다. 이를 최산 세대인 gp3 타입이 gp2 타입과 비교하여 비용과 성능 측면에서 더 낫기 때문에 클러스터 생성 시에 gp3로 기본 스토리지 클래스를 설정하는 것이 좋다.

일반적으로 스토리지 클래스 재설정 작업은 클러스터가 생성될 때 딱 한 번만 하면 되는 작업이기 때문에 테라폼으로 클러스터를 생성할 때 동시에 작업하는 것이 좋다.

기본 스토리지 클래스 설정은 storage.class.kubernetes.io/is-default-class 라는 어노테이션을 설정하는 것이다. 쿠버네티스 객체의 어노테이션을 강제 적용할 수 있는 리소스 블록인 kubernetes_annotations 가 존재한다. 해당 리소스 블록을 정의하여 메타 데이터로 name = "gp2"를 가지고 있는 스토리지 클래스 객체에 어노테이션을 강제 지정하여 기본 스토리지 설정을 해제한다. 그다음 gp3 스토리지 클래스를 생성하여 기본 클래스로 설정한다.

# gp2 storage class default false
resource "kubernetes_annotation" "sc_gp2" {
  api_version = "storage.k8s.io/v1"
  kind        = "StorageClass"
  force       = "true"
  metadata {
    name = "gp2"
  }
  annotations = {
    "storageclass.kubernetes.io/is-default-class" = "false"
  }
}

# create gp3 storage class default true
resource "kubernetes_storage_class" "sc_gp3" {
  metadata {
    name = "gp3"
    annotations = {
      "storageclass.kubernetes.io/is-default-class" = "true"
    }
  }
  storage_provisioner = "ebs.csi.aws.com"
  volume_binding_mode = "WaitForFirstConsumer"
  parameters = {
    "type"                      = "gp3"
    "csi.storage.k8s.io/fstype" = "ext4"
  }
  allow_volume_expansion = true
}

 

테라폼은 관리 대상에 포함되지 않은 인프라 리소스의 상태를 조작하는 것은 선언적 원리에 반하는 작동 방식이다.  kubernetes_annotations 리소스 블록은 기존 쿠버네티스 리소스를 덮어쓰기 보다 리소스에 추가로 지정할 메타데이터를 관리하는 새로운 리소스로 볼 수 있다. 따라서 선언적 관리의 원리를 벗어나지 않으면서도 필요한 작동을 수행한다.

 

쿠버네티스 클러스터 접근 제어

아마존 EKS 를 통해 만든 쿠버네티스 클러스터의 권한 체계는 AWS의 권한 체계인 IAM 과는 별개이다. 전통적인 방법으로는 EKS 클러스터를 생성하면 자동으로 생성되는 aws-auth라는 ConfigMap을 수정하는 방법이 널리 사용되었다. aws-auth의 전체 설정값 포맷에서 mapRoles 항목으로 AWS IAM 역할을 mapUsers 항목에 AWS IAM 사용자를 클러스터에 지정한다.

간소화되고 더 쉽게 설정하는 방법으로는 쿠버네티스 버전 1.23 이상인 EKS 클러스터에 대해, AWS API 를 통한 간소화된 쿠버네티스 접근 제어를 제공한다. ConfigMap 이 아닌 AWS API를 통해 접근 제어를 할 수 있게 되었고, 전통적인 방식인 ConfigMap 수정을 통한 접근 제어는 권장되지 않는다.

# eks 클러스터 설정
resource "aws_eks_cluster" "main" {
  name     =성 "tf-eks-cluster"
  role_arn = aws_iam_role.cluster_role.arn
  vpc_config {
    # ...
  }
  access_config {
    authentication_mode = "API"
    bootstrap_cluster_creator_admin_permissions = true
  }
}

# 액세스 항목 생성
resource "aws_eks_access_entry" "devops_team" {
    cluster_name = aws_eks_cluster.main.name
    principal_arn = "arn:aws:iam:1234123412334:role/xxxx"
    type = "STANDARD"
}

resource "aws_eks_access_policy_association" "devops_admin_policy" {
  cluster_name  = aws_eks_cluster.main.name
  principal_arn = aws_eks_access_entry.devops_team.principal_arn # 위에서 만든 Entry와 동일해야 함
  
  # AWS 관리형 정책 ARN (Cluster Admin 권한)
  policy_arn    = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"

  # 권한 범위 설정
  access_scope {
    type       = "cluster" # cluster 전체 혹은 specific namespace 지정 가능
    # namespaces = ["default"] # type이 namespace일 경우 지정
  }
}

 

아마존 EKS 의 access entries라는 리소스를 생성하는 방식으로 access 정책을 클러스터나 네임 스페이스별로 부여할 수도 있다.

 

오버프로비저닝을 위한 파드 배포

아마존 EKS 에서 노드 그룹 오토스케일링 단위를 설정하는 방법은 대표적으로 클러스터 오토스케일러와 카펜터가 있다. 그러나 두 방법 모두 CPU 나 메모리 등 노드 리소스 사용률의 한계점에 따른 스케줄링을 지원하지 않는다. 

즉 현재 존재하는 모든 노드가 꽉 차서 새 파드가 스케줄 될 공간이 없어지는 시점까지 새로운 노드가 프로비저닝 되지 않는다.

이를 위해 AWS 에서 권장하는 방법은 클러스터 오버프로비저닝이다. 우선순위가 낮은 파드들 다른 말로 플레이스홀더 파드들을 미리 생성해 두어 급하게 노드가 필요해지는 경우 플레이스홀더 파드가 있던 자리에 서비스 파드가 뜰 수 있게 하고, 갈 곳이 사라진 플레이스홀더 파드를 위해 새로운 노드가 프로비저닝 되는 방식이다.

트래픽 급증의 이유로 서비스 파드가 갑자기 큰폭으로 늘어나야 하는데, 노드를 생성하는 시간을 기다리는 것은 타임 아웃 등으로 인한 오류 확산이 일어날 위험이 있으므로, 우선순위가 낮은 파드들을 만들어 두고 필요시 그 자리를 비워 사용하게 하는 것이다. 이를 위해 파드 우선순위 적용이 필요하며 이는 쿠버네티스의 우선수위 클래스를 이용해야 한다. pause 이미지를 사용하며 자동 생성되는 어노테이션에 대해 ignore_changes 설정을 한다.

# 플레이스홀더 파드를 위한 우선순위를 생성하는 테라폼 코드
resource "kubernetes_priority_class_v1" "overprovisioning" {
  metadata {
    name = "overprovisioning"
  }

  value          = -10
  global_default = false
  description    = "placeholder pod for overprovisioning"
}

resource "kubernetes_deployment_v1" "overprovisioning" {
  metadata {
    name      = "overprovisioning"
    namespace = "default"
  }

  spec {
    # ...
    template {
      metadata {
        name = "overprovisioning_placeholder"
      }

      spec {
        priority_class_name              = kubernetes_priority_class_v1.overprovisioning.id
        termination_grace_period_seconds = 0
        container {
          name  = "reserve-resources"
          image = "registry.k8s.io/pause:3.9"
        }
      }
    }
  }

  lifecycle {
    ignore_changes = [
      spec[0].template[0].metadata[0].annotations,
    ]
  }

}

헬름 프로바이더

헬름은 쿠버네티스 객체를 테라폼의 모듈과 유사한 차트라는 것을 사용해 선언적으로 관리하기 위해 사용하는 도구이다. ArgoCD 등의 깃옵스 도구와 헬름을 함께 사용한다면 굳이 쿠버네티스 리소스 관리를 위해 테라폼을 사용할 필요가 없다. 그렇다면 테라폼의 헬름 프로바이더는 왜 필요한 것일까?

새로운 클러스터를 프로비저닝하고 부트스트래핑 하는 경우 필수로 설치해야 할 헬름차트는 EKS 모듈에 포함해서 하나의 단위로 관리하는 것이 편리하다. 이렇게 부트스트래핑을 할 때 매우 자주 사용하는 차트는 다음과 같다.

  • metrics-server: 노드와 파드의 CPU, 메모리 등 메트릭 조회를 위한 서버
  • cluster-autoscaler: 클러스터 오토 스케일링
  • ingress-nginx-controller(이제곧 deprecated): nginx 기반 인그레스 리소스 컨트롤러
  • aws-load-balancer-controller: aws 로드 밸런서 조작을 위한 컨트롤러
  • aws-ebs-csi-driver: aws ebs 기반 스토리지 csi 드라이버
  • cert-manager: tls 인증서 매니저
  • external-dns: 외부 도메인 네임 서버 사용을 위한 컨트롤러
  • datadog: 데이터독 수집 에이전트
  • argo-rollout: 아르고 롤아웃 배포

aws- 접두사로 시작하는 애플리케이션은 EKS 전용이지만, 그 외는 어떤 쿠버네티스 엔진이든 사용할 수 있다. 헬름 차트 하나를 테라폼으로 구성하는 것은 매우 간단하다.

resource "helm_release" "metrics_server" {
	name = "metrics-server"
	repository = "https://kubernetes-sigs.github.io/metrics-server/"
	chart = "metrics-server"
	version = "3.12.2"
	namespace = "kube-system"
	timeout = "1200"
}

 

다만, 테라폼으로 설치할 헬름 차트를 EKS 모듈에 선언하는 것은 불편하며, 헬름차트 관리 용이성을 높이기 위해 EKS 모듈 내부에 helm_release라는 서브 모듈을 구성해보자.

이 모듈은 다음과 같은 요구사항을 만족할 수 있도록 모듈 구성을 진행한다.

  • 특정 클러스터에 헬름 차트를 추가할 수 있다.
  • 특정 클러스터에 헬름 차트를 삭제할 수 있다.
  • 같은 차트의 경우에도 클러스터마다 세부 설정값을 다르게 적용할 수 있다.
  • aws 리소스 조작을 위해 IRSA를 추가할 수 있다.

 

헬름 프로바이더 설정하기

기존 클러스터에 접근할 수 있는 헬름 프로바이더를 생성한다. 

provider "helm" {

  kubernetes = {
    host                   = local.cluster_endpoint
    token                  = local.cluster_token
    cluster_ca_certificate = local.cluster_ca_certificate
  }
}

 

서브 모듈을 구성하기 위한 입력값 정하기

앞서 생성했던 다른 모듈들과 유사한 구조로 헬름 모듈을 만들 수 있다. YAML 파일을 통해 EKS 모듈을 호출할 때 필요한 값을 정의한다. yaml 파일에는 클러스터별 헬름 차트를 위한 입력값을 작성한다.

helm_release:
  metrics-server:
    enable: true
    chart_repo: https://kubernetes-sigs.github.io/metrics-server/
    chart_version: 3.12.2
    namespace: kube-system
    additional_values:
      replicas: 3
  external-dns:
    enable: true
    chart_repo: https://kubernetes-sigs.github.io/external-dns/
    chart_version: 1.15.0
    namespace: kube-system
    additional_values:
      policy: upsert-only

 

만약 위와 같은 명세를 사용해서 많은 클러스터를 생성하면 클러스터별로 중복되는 정보가 많아진다. 해당 파일이 클러스터별로 생성되기 때문에 관리하기 어려워질 것이다. chart_repo, chart_version 등은 클러스터에서 동일한 값을 사용할 것이고, replicas와 같은 값도 예외적인 클러스터 소수를 제외하면 공통값을 가지는 경우가 많다. 이를 위해 helm_default_values 디렉터리를 생성하고 각 차트에 대한 기본값을 명세한다.

따라서 아래와 같은 파일 구조를 사용하여 클러스터에서 사용하는 공통 헬름 차트의 기본값을 명세한다.

env
└── info_files
    ├── cluster_values
    │   └── cluster1.yaml
    └── helm_default_values
        ├── _helm_charts.yaml  # 헬름 차트 설치 시 필요한 차트 정보
        └── metrics-server.yaml  # 특정 헬름 차트의 기본값

 

_helm_charts.yaml 파일은 헬름 차트를 설치할 때 필요한 차트 이름, 레포지토리, 버전 등 차트에 대한 정보를 작성한다.

env/info_files/helm_default_values/_helm_charts.yaml

metrics-server:
  repository: https://kubernetes-sigs.github.io/metrics-server/
  version: 3.12.2
  namespace: kube-system
external-dns:
  repository: https://kubernetes-sigs.github.io/external-dns/
  version: 1.15.0
  namespace: kube-system

 

그리고 차트별 yaml 파일엔 헬름 차트를 설치할 때 필요한 설정값의 기본값을 정의한다.

env/info_files/helm_default_values/metrics-server.yaml

relicas: 3

 

이렇게 되면 각 클러스터별 명세 파일에서 헬름 차트에 대한 정보는 공통값과 기본값을 생략하여 간소하게 작성할 수 있다.

env/info_files/cluster_values/cluster1.yaml

helm_release:
  metrics-server:
    enable: true
  external-dns:
    enable: true
    overwrite_values:
      policy: sync
      serviceAccount.create: true

 

이런 설정을 사용하면 클러스터에 헬름 차트를 추가, 삭제하고 같은 차트의 경우에도 클러스터마다 세부 설정값을 다르게 적용할 수 있다.

이제 aws 리소스 조작을 위해 IRSA를 추가할 수 있도록 개선해 보자.

IRSA는 쿠버네티스 파드에 AWS IAM 권한을 적용하기 위해 사용한다. 파드가 사용할 서비스 어카운트 객체에 필요한 IAM 정책을 가진 IAM 역할의 ARN을 어노테이션으로 지정해야 한다. 이때 여기서 지정하는 IAM 역할은 해당 EKS 클러스터의 OIDC 프로바이더를 신뢰해야 한다.

마지막 요구사항에 맞춰 IRSA를 사용할 수 있도록 입력값 명세에 IAM 권한을 정의해야 한다. 예를 들어 external-dns 차트를 정의할 경우 Route53 DNS 레코드를 생성할 수 있는 IAM 정책이 필요하다. 이를 위해 irsa_policies 디렉터리를 만들어 AWS IAM 정책의 표준 형식인 JSON 형식으로 정책 파일을 추가한다.

env/info_files/irsa_policies/external-dns.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "route53:ChangeResourceRecordSets"
            ],
            "Resource": [
                "arn:aws:route53:::hostedzone/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "route53:ListResourceRecordSets",
                "route53:ListHostedZones",
                "route53:ListTagsForResource"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

 

json으로 선언된 IAM 정책을 만들어 EKS 모듈 내부에서 IAM 정책과 이를 사용하는 IAM 역할을 모두 생성하도록 할 수 있다. 이렇게 생성하는 IAM 역할을 external-dns의 헬름차트 서브 모듈로 사용할 수 있어야 한다.

env/info_files/helm_default_values/external-dns.yaml

policy: upsert-only
serviceAccount:
  create: true
  annotations:
    "eks.amazonaws.com/role-arn": "${irsa_arn}"

 

서브 모듈을 호출하는 방법 정하기

먼저 EKS 모듈에서 서브 모듈로 넘겨야 하는 리소스 중 하나로 IRSA에 필요한 IAM 역할을 만들기 위해 OIDC 프로바이더 리소스를 생성해야 한다.

modules/eks/eks_cluster.tf

locals {
  cluster_name = "example"
}

resource "aws_eks_cluster" "this" {
  name = local.cluster_name

  vpc_config {
    subnet_ids = ["subnet-12345678", "subnet-87654321"]
  }

  role_arn = "arn:aws:iam::123456789012:role/eks-cluster-role"
}

locals {
  issuer = aws_eks_cluster.this.identity[0].oidc[0].arn
}

data "aws_partition" "current" {}

data "tls_certificate" "this" {
  url = local.issuer
}

resource "aws_iam_openid_connect_provider" "this" {
  client_id_list  = ["sts.${data.aws_partition.current.dns_suffix}"]
  thumbprint_list = [data.tls_certificate.this.certificates[0].sha1_fingerprint]
  url             = local.issuer
}

 

앞서 yaml 등으로 작성한 파일을 attribute라는 변수로 받는다고 하면 var.attribute.helm_release로 접근하면 해당 클러스터가 사용할 헬름 차트의 정보를 얻을 수 있다. 이에 따라 eks 클러스터 모듈의 헬름 서브 모듈을 호출하는 부분은 아래와 같이 작성할 수 있다.

locals {
  helm_release_info = var.attribute.helm_release
}

module "helm_release" {
  source = "../helm_release"
  for_each = {
    for k, v in local.helm_release_info : k => v
    if v.enable
  }

  name      = each.key
  attribute = each.value

  cluster_name = local.cluster_name
  cluster_oidc = {
    arn = aws_iam_openid_connect_provider.this.arn
    url = aws_iam_openid_connect_provider.this.url
  }
}

 

서브 모듈 구성하기

조건에 맞는 파일로 구성된 디렉터리 구조와 IRSA를 사용하기 위한 OIDC 설정이 완료되었으므로 helm 서브 모듈을 만들어보자.

1. 임시 변수 설정하기

variable "name" {}
variable "attribute" {}
variable "cluster_name" {}
variable "cluster_oidc" {}

 

2. 헬름 차트 설정값 선언

locals {
  helm_default_values_path = "${path.root}/env/info_files/helm_default_values"
  helm_chart_info          = yamldecode(file("${local.helm_default_values_path}/_helm_charts.yaml"))[var.name]
  chart_repo               = coalesce(var.attribute.repository, local.helm_chart_info.repository)
  chart_version            = coalesce(var.attribute.version, local.helm_chart_info.version)
  namespace                = coalesce(var.attribute.namespace, local.helm_chart_info.namespace)
}

기본 chart 설정값과 다른 설정값을 사용할 수 있도록 하기 위해 var.attribute의 값을 사용하거나 기본값을 사용할 수 있도록 coalesce 함수를 사용한다.

 

3. IRSA 구성

IRSA 추가하는 설정의 경우 AWS 리소스 사용 여부에 따라 IRSA 설정 여부가 달라진다. 따라서 IRSA를 위한 policy json 파일이 존재하는지 여부를 확인하여 해당 정책 파일이 있는 경우만 IRSA 설정을 할 수 있도록 구성한다.

# IRSA
locals {
  irsa_policies_path = "${path.root}/env/info_files/irsa_policies"
  irsa_policies_set = [
    for p in fileset(local.irsa_policies_path, "*.json") : trimsuffix(p, ".json")
  ]

  create_irsa = contains(local.irsa_policies_set, var.name)
}

 

IRSA 가 정상적으로 작동하기 위해서는 IAM 역할이 헬름 차트가 설치될 쿠버네티스의 클러스터 OIDC 프로바이더를 신뢰해야 한다. 이를 위해 IAM 역할을 생성할 때 assume_role_policy 인수에 OIDC 프로바이더 신뢰를 위한 신뢰 관계 생성을 위한 IAM 정책을 지정해주어야 한다. 이를 위한 템플릿 파일은 다음과 같다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "${oidc_arn}"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "${oidc_url}:sub": "system:serviceaccount:${namespace}:*",
                    "${oidc_url}:aud": "sts.amazonaws.com"
                }
            }
        }
    ]
}

예시 정책의 경우 허용된 서비스 어카운트 이름은 와일드카드를 사용했지만, 실제로는 보안적인 측면에서 각 서비스 어카운트의 이름을 명시적으로 지정하는 것이 안전하다. 이를 달성하기 위해선 helm_release 서브 모듈에서 서비스 어카운트를 생성하도록 구성해야 한다.

 

앞서 선언한 irsa 로컬 블록과 신뢰 정책 템플릿을 사용하여 IRSA를 위한 IAM 역할과 정책을 만들 수 있다.

# IAM Role for IRSA
resource "aws_iam_role" "this" {
  count = local.create_irsa ? 1 : 0

  name = "${var.cluster_name}-${var.name}"
  assume_role_policy = templatefile("${path.module}/irsa_assume_role_template.json", {
    oidc_arn  = var.cluster_oidc.arn
    oidc_url  = var.cluster_oidc.url
    namespace = local.namespace
  })
}

# IAM Policy for IRSA
resource "aws_iam_role_policy" "this" {
  count = local.create_irsa ? 1 : 0

  name   = var.name
  role   = aws_iam_role.this[count.index].id
  policy = file("${local.irsa_policies_path}/${var.name}.json")
}

 

4. helm_release 리소스 블록

# helm release
locals {
  helm_default_values = templatefile("${local.helm_default_values_path}/${var.name}.yaml", {
    irsa_arn = try(aws_iam_role.this[0].arn, "")
  })

  helm_overwrite_values = lookup(var.attribute, "overwrite_values", {})
}

resource "helm_release" "this" {
  name       = var.name
  repository = local.chart_repo
  version    = local.chart_version
  chart      = var.name
  namespace  = local.namespace
  timeout    = "1200"

  values = [
    local.helm_default_values
  ]

  dynamic "set" {
    for_each = local.helm_overwrite_values
    content {
      name  = set.key
      value = set.value
    }
  }
}

values는 helm 커맨드의 -f 옵션과 동일하고 set 블록은 helm -set 옵션과 동일하다. 헬름 커맨드라인 명령어에서는 set의 우선순위가 더 높기 때문에 set으로 overwrite 하기 위한 값들을 정의한다.

 

6. 변수 유효성 검사

# helm release
locals {
  helm_default_values = templatefile("${local.helm_default_values_path}/${var.name}.yaml", {
    irsa_arn = try(aws_iam_role.this[0].arn, "")
  })

  helm_overwrite_values = lookup(var.attribute, "overwrite_values", {})
}

resource "helm_release" "this" {
  name       = var.name
  repository = local.chart_repo
  version    = local.chart_version
  chart      = var.name
  namespace  = local.namespace
  timeout    = "1200"

  values = [
    local.helm_default_values
  ]

  dynamic "set" {
    for_each = local.helm_overwrite_values
    content {
      name  = set.key
      value = set.value
    }
  }
}

커스텀 리소스와 kubectl 프로바이더

쿠버네티스와 헬름 프로바이더를 사용하여 클러스터 프로비저닝을 수행할 수 있게 되었다. 이제 최초 프로비저닝 시에 커스텀 리소스를 생성해서 사용하는 경우를 살펴보자. Istio, ArgoCD 등 여러 쿠버네티스 기반 애플리케이션의 경우 쿠버네티스 기본 객체뿐 아니라 CR 도 따로 정의하여 사용한다.

CR의 경우 쿠버네티스 프로바이더를 사용하면 kubernetes_manifest 리소스 블록을 사용해 생성할 수 있다. 이를 위해선 CRD 가 클러스터 안에 설정되어 있어야 한다. 이때 클러스터를 최초로 프로비저닝 하는 경우, 테라폼 플랜 시점에 쿠버네티스 API를 통해 설치하려는 매니페스트에 대한 유효성을 검증하는데 클러스터에는 CRD 가 없기 때문에 CR에 대한 검증을 올바르게 할 수 없다.

따라서 kubernetes_manifest로는 CR 객체를 클러스터 프로비저닝과 동시에 만들 수 없고 이 방법을 사용하기 위해선 CRD 먼저 클러스터에 배포한 후 사용할 수 있다.

클러스터 프로비저닝 시 반드시 만들어야 하는 CR 이 있다면 해결할 수 있는 방법은 헬름 프로바이더를 이용하거나 커뮤니티 프로바이더인 kubectl 프로바이더를 이용하는 방법 두 가지가 있다.

 

헬름 차트로 Custom Resource 객체 배포하기

첫 번째 방법은 생성해야 하는 CR 객체를 커스텀 헬름 차트로 만든 후 해당 차트를 헬름 프로바이더를 사용해 배포하는 것이다. 그다음 CRD 가 만들어진 다음에 해당 차트를 배포하도록 테라폼 리소스 블록의 의존성을 지정하면 된다. 이 방법은 프라이빗 레포에서 관리하는 차트인 경우 사용하지 못하며, 헬름 차트와 템플릿에 대한 이해가 필요하기 때문에 번거로울 수 있다.

 

Kubectl 프로바이더 활용하기

두 번째 방법은 커뮤니티 프로바이더인 kubectl 프로바이더를 사용하는 것이다. 커뮤니티 프로바이더는 오픈소스로 관리되어 여러 개발자들이 각자의 방식으로 개발하여 레지스트리에 올리는 것으로 kubectl 프로바이더는 이런 커뮤니티 티어의 프로바이더이다.

가장 많이 사용하는 kubectl 프로바이더는 gavinbunney/kubectl 프로바이더이다. 이번 글에서는 alekc/kubectl 프로바이더를 사용해 클러스터 프로비저닝에 CR을 생성할 수 있는 방법을 확인하자. 참고로 현재 두 프로바이더 모두 글을 쓰는 시점을 기준으로 1년 정도 업데이트를 하지 않고 있다.

alekc/kubectl 프로바이더(이하 kubectl 프로바이더)를 사용하는 설정 방법은 다음과 같다.

terraform {
  required_providers {
    kubectl = {
      source  = "alekc/kubectl"
      version = "2.1.3"
    }
  }
}

provider "kubectl" {
  host                   = local.cluster_endpoint
  token                  = local.cluster_token
  cluster_ca_certificate = local.cluster_ca_certificate
  load_config_file       = false

}

공식 프로바이더가 아닌 다른 프로바이더를 사용하는 경우 반드시 required_providers에 프로바이더의 소스 정보를 명세해야 한다. 또한 처음 클러스터를 프로비저닝 하는 시점에 로컬 kubeconfig 파일을 읽을 수 없기 때문에 load_config_file 인수는 false로 설정해야 한다.

 

kubectl_manifest 리소스를 사용해 카펜터의 NodePool 리소스의 매니페스트를 배포한다. 

locals {
  name = "k8s_node_pool_demo"
  k8s_labels = {

  }
}

locals {
  disruption = var.attribute.disruption
  taints     = var.attribute.taints

  nodepool_manifest = {
    apiVersion = "karpenter.sh/v1"
    kind       = "NodePool"
    metadata = {
      name   = local.name
      labels = local.k8s_labels
    }

    spec = {
      disruption = {
        consolidationPolicy = local.disruption.consolidationPolicy
        consolidateAfter    = local.disruption.consolidateAfter
        budgets = [
          for b in local.disruption.budgets : {
            for k, v in b : k => v if v != null
          }
        ]
      }
      template = {
        metadata = {
          labels = local.k8s_labels
        }
        spec = {
          expireAfter = local.node.spec.expireAfter
          nodeClassRef = {
            group = "karpenter.k8s.aws"
            kind  = "EC2NodeClass"
            name  = local.name
          }
          taints = [
            for k, v in local.taints : {
              key    = k
              value  = v
              effect = "NoSchedule"
            }
          ]

          requirements = [
            {
              key      = "kubernetes.io/arch"
              operator = "In"
              values   = local.node_spec.image_arch
            },
            {
              key      = "kubernetes.io/os"
              operator = "In"
              values   = local.node_spec.image_os
            },
            {
              key      = "karpenter.sh/capacity-type"
              operator = "In"
              values   = local.node_spec.instance_capacity
            },
            {
              key      = "karpenter.k8s.aws/instance-family"
              operator = "In"
              values   = local.node_spec.instance_family
            },
            {
              key      = "karpenter.k8s.aws/instance-size"
              operator = "In"
              values   = local.node_spec.instance_size
            }
          ]
        }
      }
    }
  }

}

resource "kubectl_manifest" "node_pool" {
  yaml_body = yamlencode(local.nodepool_manifest)
}

 

kubectl_manifest 리소스 블록을 사용해 EC2 NodeClass 객체를 생성한다.

locals {
  volume_size_list = var.attribute.volume_size_list
  device_names     = ["sda1", "sdf", "sdg", "sdh", "sdi", "sdj", "sdk", "skl", "sdm", "sdn", "sdo", "sdp"]
  device_list      = slice(local.device_names, 0, length(local.volume_size_list))
  device_mapping   = zipmap(local.device_list, local.volume_size_list)

  nodeclass_manifest = {
    apiVersion = "karpenter.k8s.aws/v1"
    kine       = "EC2NodeClass"

    metadata = {
      name   = local.name
      labels = local.k8s_labels
    }

    spec = {
      instanceProfile = var.node_role
      tags            = local.module_tags
      amiSelectorTerms = [
        for i in local.node_spec.image_alias : { alias = i }
      ]

      subnetSelectorTerms = [
        for i in var.subnet_ids : { id = i }
      ]

      securityGroupSelectorTerms = [{ id = var.node_sg }]

      blockDeviceMappings = [
        for device, size in local.device_mapping : {
          deviceName = "/dev/${device}"
          ebs = {
            volumeSize          = "${size}Gi"
            volumeType          = "gp3"
            iops                = 3000
            throughput          = 150
            encrypted           = false
            deleteOnTermination = true
          }
        }
      ]
    }

  }
}

resource "kubectl_manifest" "node_class" {
  yaml_body = yamlencode(local.nodeclass_manifest)
}

 

두 CR 모두 kubectl_manifest 리소스 블록을 사용하여 yaml 형태의 매니페스트를 클러스터에 반영하는 방법을 사용한다. yaml_body 인수 안에 yaml 형식으로 인코딩 된 매니페스트를 넘기는 방식으로 리소스를 정의할 수 있다.