Skip to content
ioob.dev
Go back

Terraform 12편 — Kubernetes와 Helm 프로바이더

· 8분 읽기
Terraform 시리즈 (12/15)
  1. Terraform 1편 — Terraform이란
  2. Terraform 2편 — 설치와 첫 배포
  3. Terraform 3편 — HCL 문법
  4. Terraform 4편 — 변수와 출력
  5. Terraform 5편 — 프로바이더
  6. Terraform 6편 — 리소스와 의존성
  7. Terraform 7편 — 데이터 소스와 Import
  8. Terraform 8편 — State 관리
  9. Terraform 9편 — 모듈
  10. Terraform 10편 — 반복과 조건
  11. Terraform 11편 — 워크스페이스와 환경 분리
  12. Terraform 12편 — Kubernetes와 Helm 프로바이더
  13. Terraform 13편 — CI/CD 통합
  14. Terraform 14편 — 테스트와 정책
  15. Terraform 15편 — 실전 패턴과 함정
Table of contents

Table of contents

Terraform이 Kubernetes까지?

Terraform은 클라우드 인프라를 만드는 도구다. EKS 클러스터를 만드는 데 쓰는 건 익숙할 것이다. 그런데 그 클러스터 에 Deployment를 만들거나 Helm 차트를 배포하는 것도 Terraform으로 할 수 있다.

“그걸 왜 Terraform으로 하지? kubectl이나 ArgoCD가 있잖아.” 맞는 질문이다. 결론부터 말하면, Terraform이 Kubernetes를 관리하는 게 항상 좋은 선택은 아니다. 하지만 잘 맞는 상황이 분명히 있다. 그 경계를 정확히 그리는 게 이번 편의 목표다.

flowchart TB
    subgraph "두 접근법"
        direction LR
        TF["Terraform"]
        ArgoCD["ArgoCD / Flux\n(GitOps)"]
    end

    TF --> TFCase["인프라 일부로서\n클러스터 부트스트랩,\n정적 리소스"]
    ArgoCD --> GitOpsCase["앱 배포 워크플로,\n자주 바뀌는 매니페스트"]

kubernetes 프로바이더 기본

먼저 프로바이더를 선언하고 인증을 설정한다.

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

provider "kubernetes" {
  # 옵션 1: kubeconfig 파일 사용
  config_path    = "~/.kube/config"
  config_context = "my-cluster"
}

이러면 로컬 kubeconfig를 통해 클러스터에 접근한다. 사람이 수동으로 apply할 때는 괜찮지만, CI에서는 다른 방식을 써야 한다.

EKS에서 Terraform으로 클러스터를 만들고 바로 그 클러스터에 리소스를 배포하는 일반적인 패턴은 이렇다.

data "aws_eks_cluster" "cluster" {
  name = module.eks.cluster_name
}

data "aws_eks_cluster_auth" "cluster" {
  name = module.eks.cluster_name
}

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
}

EKS API 엔드포인트와 CA 인증서를 받아서 프로바이더에 주입한다. aws_eks_cluster_auth는 실행 시점마다 새 토큰을 발급받는 데이터 소스라, 토큰 만료 걱정 없이 쓸 수 있다.

Kubernetes 리소스 만들기

이제 클러스터에 리소스를 만들어보자.

resource "kubernetes_namespace" "monitoring" {
  metadata {
    name = "monitoring"

    labels = {
      "pod-security.kubernetes.io/enforce" = "baseline"
    }
  }
}

resource "kubernetes_config_map" "app_config" {
  metadata {
    name      = "app-config"
    namespace = kubernetes_namespace.monitoring.metadata[0].name
  }

  data = {
    "config.yaml" = yamlencode({
      server = {
        port = 8080
        host = "0.0.0.0"
      }
      features = {
        caching = true
      }
    })
  }
}

resource "kubernetes_deployment" "nginx" {
  metadata {
    name      = "nginx"
    namespace = kubernetes_namespace.monitoring.metadata[0].name
  }

  spec {
    replicas = 3

    selector {
      match_labels = {
        app = "nginx"
      }
    }

    template {
      metadata {
        labels = {
          app = "nginx"
        }
      }

      spec {
        container {
          name  = "nginx"
          image = "nginx:1.25"

          port {
            container_port = 80
          }

          resources {
            limits = {
              cpu    = "500m"
              memory = "256Mi"
            }
            requests = {
              cpu    = "100m"
              memory = "128Mi"
            }
          }
        }
      }
    }
  }
}

문법이 HCL이라는 점 빼면 Kubernetes YAML과 거의 1:1 대응이다. 중첩 블록이 많아서 길지만 구조는 명확하다.

kubernetes_manifest — 임의의 CRD 다루기

kubernetes_deployment, kubernetes_service 같은 리소스 타입은 프로바이더가 미리 정의한 것들이다. Istio의 VirtualService나 Argo Rollouts의 Rollout 같은 Custom Resource는 별도 프로바이더가 없을 수 있다.

이럴 때 kubernetes_manifest를 쓰면 임의의 매니페스트를 그대로 넘길 수 있다.

resource "kubernetes_manifest" "virtual_service" {
  manifest = yamldecode(file("${path.module}/manifests/virtualservice.yaml"))
}

또는 인라인으로.

resource "kubernetes_manifest" "prometheus_rule" {
  manifest = {
    apiVersion = "monitoring.coreos.com/v1"
    kind       = "PrometheusRule"

    metadata = {
      name      = "high-error-rate"
      namespace = "monitoring"
    }

    spec = {
      groups = [{
        name = "api.rules"
        rules = [{
          alert = "HighErrorRate"
          expr  = "rate(http_requests_total{status=~\"5..\"}[5m]) > 0.05"
          for   = "5m"
          labels = {
            severity = "warning"
          }
          annotations = {
            summary = "High error rate detected"
          }
        }]
      }]
    }
  }
}

주의할 점: kubernetes_manifest는 첫 plan 시점에 해당 CRD가 클러스터에 이미 설치되어 있어야 한다. CRD를 설치하는 Helm 차트와 이 리소스를 같은 Terraform 실행에서 다루면 순서 문제가 생길 수 있다. CRD 설치는 별도 Terraform 또는 Helm으로 먼저 적용하는 게 안전하다.

helm 프로바이더 — Helm 차트 배포

helm 프로바이더는 Terraform에서 Helm 차트를 배포하게 해준다.

terraform {
  required_providers {
    helm = {
      source  = "hashicorp/helm"
      version = "~> 2.13"
    }
  }
}

provider "helm" {
  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
  }
}

Kubernetes 프로바이더와 비슷한 인증 설정을 kubernetes 블록 안에 넣는다.

이제 차트를 배포한다.

resource "helm_release" "ingress_nginx" {
  name       = "ingress-nginx"
  namespace  = "ingress-nginx"
  repository = "https://kubernetes.github.io/ingress-nginx"
  chart      = "ingress-nginx"
  version    = "4.11.0"

  create_namespace = true

  values = [
    yamlencode({
      controller = {
        replicaCount = 2
        service = {
          type = "LoadBalancer"
          annotations = {
            "service.beta.kubernetes.io/aws-load-balancer-type" = "nlb"
          }
        }
        resources = {
          requests = {
            cpu    = "100m"
            memory = "128Mi"
          }
        }
      }
    })
  ]
}

values 블록에 yamlencode로 YAML을 인라인 선언할 수 있다. 개별 값만 오버라이드하고 싶으면 set 블록을 쓸 수도 있다.

resource "helm_release" "grafana" {
  name       = "grafana"
  namespace  = "monitoring"
  repository = "https://grafana.github.io/helm-charts"
  chart      = "grafana"
  version    = "7.3.0"

  set {
    name  = "adminPassword"
    value = var.grafana_admin_password
  }

  set {
    name  = "persistence.enabled"
    value = "true"
  }

  set {
    name  = "persistence.size"
    value = "10Gi"
  }
}

간단한 값 몇 개 바꾸는 정도면 set이 편하다. 설정이 많으면 values가 더 깔끔하다.

클러스터 부트스트랩 — 적합한 유즈 케이스

Terraform으로 Kubernetes 리소스를 다루는 게 확실히 좋은 경우가 있다. 클러스터 부트스트랩이다.

새 EKS 클러스터를 만들었다고 해보자. 이 클러스터가 제대로 돌아가려면 몇 가지 기본 컴포넌트가 먼저 설치돼야 한다.

이 컴포넌트들은 클러스터 생성과 한 몸처럼 움직여야 한다. EKS가 만들어지자마자 즉시 필요한 것들이다. ArgoCD를 쓰려 해도, ArgoCD 자체를 누가 설치하는가 하는 문제가 생긴다.

flowchart LR
    TF["Terraform"] -->|"1. 인프라 생성"| EKS["EKS 클러스터"]
    TF -->|"2. 부트스트랩"| Boot["aws-lb-controller\nexternal-dns\ncert-manager\nArgoCD"]
    Boot -->|"3. 이후 배포는\n위임"| ArgoCD["ArgoCD가\n앱 배포 관리"]

이 단계까지는 Terraform이 자연스럽다. 인프라 레이어에서 한 번 만들고 끝나는 것들이고, 자주 바뀌지 않고, 변경할 때도 대부분 버전 업그레이드 정도다.

코드로 보면 이런 모습이다.

# 1. EKS 클러스터 생성 (모듈 사용)
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"

  cluster_name    = "prod-eks"
  cluster_version = "1.29"
  # ... 나머지 설정
}

# 2. IRSA용 OIDC 프로바이더 (모듈이 자동 생성)

# 3. aws-load-balancer-controller 설치
resource "helm_release" "aws_load_balancer_controller" {
  name       = "aws-load-balancer-controller"
  namespace  = "kube-system"
  repository = "https://aws.github.io/eks-charts"
  chart      = "aws-load-balancer-controller"

  set {
    name  = "clusterName"
    value = module.eks.cluster_name
  }

  set {
    name  = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn"
    value = module.aws_lb_controller_irsa.iam_role_arn
  }

  depends_on = [module.eks]
}

# 4. ArgoCD 설치
resource "helm_release" "argocd" {
  name             = "argocd"
  namespace        = "argocd"
  repository       = "https://argoproj.github.io/argo-helm"
  chart            = "argo-cd"
  version          = "6.7.0"
  create_namespace = true

  values = [file("${path.module}/argocd-values.yaml")]
}

# 5. ArgoCD 부트스트랩 Application (App of Apps)
resource "kubernetes_manifest" "root_app" {
  manifest = yamldecode(file("${path.module}/root-app.yaml"))
  depends_on = [helm_release.argocd]
}

여기까지가 Terraform이 하는 일이다. 이 뒤부터는 root-app.yaml이 Git의 apps/ 디렉토리를 바라보고, 실제 애플리케이션 배포는 ArgoCD가 맡는다. Terraform은 기초를 닦고 빠진다.

Terraform vs ArgoCD — 무엇을 맡길까

클러스터를 어떻게 관리할지 결정할 때 핵심 질문은 이거다. “이 리소스는 얼마나 자주 바뀌는가?”

flowchart TB
    Start["관리할 K8s 리소스"] --> Freq{"변경 빈도"}

    Freq -->|"매일, 매주\n(앱 배포, 스케일링)"| ArgoCD["ArgoCD / Flux\n(GitOps)"]
    Freq -->|"분기, 연간\n(클러스터 컴포넌트)"| TF["Terraform"]
    Freq -->|"한 번 만들고 끝\n(초기 부트스트랩)"| TF

    ArgoCD -.-> Reason1["빠른 피드백,\nRollback,\n개발자 친화"]
    TF -.-> Reason2["인프라와 한 호흡,\nstate 기반 관리,\ndrift 감지"]

Terraform이 적합한 영역

ArgoCD/Flux가 적합한 영역

애매한 영역 — 상황 판단

이 경계를 뒤섞으면 관리 주체가 불명확해진다. Deployment 하나를 Terraform과 ArgoCD가 동시에 관리하면, 한쪽에서 수정한 게 다른 쪽에서 되돌아가는 상황이 반복된다. 한 리소스는 한 도구가 관리한다는 원칙을 지키는 게 중요하다.

Terraform으로 Kubernetes를 관리할 때의 한계

실무에서 몇 번 부딪혀봐야 알게 되는 것들이다.

1) state 크기 폭발

수백 개의 K8s 리소스를 Terraform으로 관리하면 state 파일이 어마어마하게 커진다. terraform plan 속도가 느려지고, CI 시간도 길어진다.

2) 임시 수정이 drift를 만든다

장애 대응 중 kubectl scale이나 kubectl edit으로 긴급 조치를 하면, 그 순간부터 Terraform state와 실제 상태가 어긋난다. 다음 apply에서 의도치 않게 되돌아간다.

3) 롤백이 번거롭다

ArgoCD는 Git revert로 롤백이 즉시 일어난다. Terraform에서 롤백하려면 코드를 되돌리고 apply를 쳐야 한다. 긴급 상황에서 번거롭다.

4) 개발자 접근성

앱 배포를 개발자가 직접 한다면, Terraform은 러닝커브가 있다. ArgoCD의 UI나 manifest PR이 더 친숙하다.

실전 구성 예시

필자가 자주 쓰는 구성은 이렇다.

infra-repo/
├── envs/prod/
│   ├── network/          # VPC, 서브넷 (Terraform)
│   ├── cluster/          # EKS 클러스터 (Terraform)
│   └── bootstrap/        # aws-lb-controller, ArgoCD 등 (Terraform + Helm)

apps-repo/
├── apps/                 # ArgoCD Application 매니페스트
└── charts/               # 각 앱의 Helm 차트 또는 Kustomize

Terraform은 클러스터 레벨 관리, ArgoCD는 애플리케이션 레벨 배포. 역할이 명확히 나뉜다.

자주 부딪히는 문제와 해법

문제: 클러스터 인증 토큰 만료

장시간 실행되는 CI에서 aws_eks_cluster_auth가 발급한 토큰이 중간에 만료되는 경우가 있다.

해법: exec 플러그인 방식으로 바꾸기.

provider "kubernetes" {
  host                   = data.aws_eks_cluster.cluster.endpoint
  cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority[0].data)

  exec {
    api_version = "client.authentication.k8s.io/v1beta1"
    command     = "aws"
    args        = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
  }
}

exec는 필요할 때마다 AWS CLI로 새 토큰을 받아온다. 장시간 apply에도 안전하다.

문제: CRD가 없어서 plan이 실패

kubernetes_manifest는 plan 시점에 CRD가 필요하다.

해법: CRD 설치와 CR 생성을 별도 Terraform 프로젝트로 분리하거나, Helm 차트로 CRD까지 함께 설치.

문제: Helm 값에 민감 정보가 들어감

helm_releasevalues에 넣은 문자열은 state에 저장된다. 평문 비밀번호를 넣으면 state에 남는다.

해법: Kubernetes Secret으로 분리하거나 External Secrets Operator를 쓴다. Secret 참조만 values에 넣는다.


Terraform으로 Kubernetes를 관리하는 건 만능이 아니다. 변경이 드물고 인프라 레이어와 밀접한 것에만 쓰고, 애플리케이션 배포는 GitOps 도구에 맡기는 게 대부분 팀에서 잘 맞는 조합이다. 경계를 명확히 긋는 게 장기 운영의 핵심이다.

다음 편에서는 CI/CD 통합을 다룬다. GitHub Actions와 Atlantis로 Terraform을 어떻게 자동화하는지, 승인 워크플로우와 시크릿 관리를 함께 살펴본다.

13편: CI/CD 통합


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Terraform 11편 — 워크스페이스와 환경 분리
Next Post
Terraform 13편 — CI/CD 통합