IaC

Terraform - Kubernetes 연동

Operation CWAL 2022. 6. 5. 01:41

Cluster Bootstrap?

일반적으로 K8s 클러스터를 생성한 다음, 바로 Application을 배포하기 보단 원활한 클러스터 운영을 위해 시스템 애플리케이션 및 스택(Argo CD, Prometheus, Grafana 등)을 먼저 설치하는 경우가 많다. 클러스터가 얼마 되지 않고 앞으로 추가되는 경우도 없다면 큰 문제가 없지만, 계속해서 운영해야할 클러스터가 늘어난다면 이런 시스템 애플리케이션을 일종의 Bootstrap처럼 기본 제공하는 방안을 고려할 필요가 있다.

 

이 페이지는 Terraform을 통해 K8s 리소스(ex: Deployment, Service)를 생성하는 방법에 대해 설명한다. 다만 클러스터를 생성하는 과정은 벤더나 Public/Private 여부 등 환경에 따라 큰 차이가 있으므로 다루지 않는다.

 

Option 1. 각 K8s 리소스에 대응하는 kubernetes Provider의 리소스로 컨버전

Manifest가 비교적 간단하고, CRD가 아닌 경우 선택 가능한 옵션이다. Provider 선언시, kubeconfig 파일을 읽어오거나 Cluster 인증서를 사용하는 방식을 선택할 수 있다.

#kubeconfig 파일 방식
provider "kubernetes" {
  config_path    = "~/.kube/config"
  config_context = "my-context"
}
#인증서 기반
provider "kubernetes" {
  host = "https://cluster_endpoint:port"

  client_certificate     = file("~/.kube/client-cert.pem")
  client_key             = file("~/.kube/client-key.pem")
  cluster_ca_certificate = file("~/.kube/cluster-ca-cert.pem")
}

아래 코드는 test라는 namespace에 간단한 nginx 컨테이너를 Deployment 리소스로 배포하는 예시이다. 우리가 K8s에서 일반적으로 사용하는 YAML 포맷을 그대로 사용할 수 없기 때문에 HCL(Hashicorp Configuration Language)에 맞게 변경하는 작업이 필요하다. 대부분의 필드가 1:1로 매칭되기 때문에 큰 어려움은 없으나, YAML 파일이 여러개이거나, Manifest가 길고 복잡한 경우엔 코드가 길어지고 손이 많이 갈 수 밖에 없다.

resource "kubernetes_namespace" "test" {
  metadata {
    name = "test"
  }
}

resource "kubernetes_deployment" "test" {
  metadata {
    name = "test"
    namespace= kubernetes_namespace.test.metadata.0.name
  }
  spec {
    replicas = 2
    selector {
      match_labels = {
        app = "test"
      }
    }
    template {
      metadata {
        labels = {
          app  = "test"
        }
      }
      spec {
        container {
          image = "nginx:1.19.4"
          name  = "nginx"

          resources {
            limits = {
              memory = "512M"
              cpu = "1"
            }
            requests = {
              memory = "256M"
              cpu = "50m"
            }
          }
        }
      }
    }
  }
}

그리고 Kubernetes의 기본 리소스가 아닌 써드파티에서 정의한 CRD(ex: Istio VirtualService, DestinationRule 등)에 해당하는 리소스를 kubernetes Provider가 직접 제공할 수 없다는 문제가 있다. 이러한 점을 보완하기 위해 kubernetes_manifest 라는 리소스도 제공하고 있지만, 마찬가지로 YAML 파일을 그대로 사용할 수는 없으며, 아래와 같이 HCL 양식에 맞게 수정해야 한다.

resource "kubernetes_manifest" "test-crd" {
  manifest = {
    apiVersion = "apiextensions.k8s.io/v1"
    kind       = "CustomResourceDefinition"

    metadata = {
      name = "testcrds.hashicorp.com"
    }

    spec = {
      group = "hashicorp.com"

      names = {
        kind   = "TestCrd"
        plural = "testcrds"
      }

      scope = "Namespaced"

      versions = [{
        name    = "v1"
        served  = true
        storage = true
        schema = {
          openAPIV3Schema = {
            type = "object"
            properties = {
              data = {
                type = "string"
              }
              refs = {
                type = "number"
              }
            }
          }
        }
      }]
    }
  }
}

다행히도 built-in function 'yamldecode()'나 tfk8s같은 Tool로 YAML -> HCL의 자동 변환을 지원하기 때문에 어느 정도의 수고는 덜 수 있지만, YAML 파일 내용이 Terraform 코드에 반영되는 것은 피할 수 없다.

 

Option 2. Helm chart로 재구성 후 Helm Provider를 통한 배포

kubernetes Provider 대신 helm Provider를 사용해서 Application을 배포할 수 있다. 우선 Provider를 선언하는 방식은 kubernetes와 거의 동일하다(사실 큰 차이가 날 수 없다).

#kubeconfig 파일 방식
provider "helm" {
  kubernetes {
    config_path = "~/.kube/config"
  }
}
provider "helm" {
  kubernetes {
    host     = "https://cluster_endpoint:port"

    client_certificate     = file("~/.kube/client-cert.pem")
    client_key             = file("~/.kube/client-key.pem")
    cluster_ca_certificate = file("~/.kube/cluster-ca-cert.pem")
  }
}

helm Provider가 지원하는 리소스는 단 하나, 'helm_release' 밖에 존재하지 않는다. 다음은 bitnami repo로부터 redis 차트를 설치하는 Terraform 코드의 예시이다. values.yaml 파일을 직접 읽어오거나, set 블록을 통해 설정 가능한 것을 알 수 있다.

resource "helm_release" "example" {
  name       = "my-redis-release"
  repository = "https://charts.bitnami.com/bitnami"
  chart      = "redis"
  version    = "6.0.1"

  values = [
    "${file("values.yaml")}"
  ]

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

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

  set {
    name  = "service.annotations.prometheus\\.io/port"
    value = "9127"
    type  = "string"
  }
}

 

이 방식은 기존 Helm Chart가 존재하는 경우 쉽게 사용할 수 있다는 장점이 있다.  그렇지 않다면 기존 YAML 파일들을 Helm chart로 전환하는데 많은 노력과 시간이 필요하며, Private Helm Repo도 구성해야 한다는 부담감이 다소 존재한다.

 

Option 3. null_resource의 local-exec Provisioner에서 kubectl 명령어 활용

null_resource는 특정 리소스에 묶이지 않고, 1회성의 프로비저닝 작업을 수행하기 위한 리소스 타입이다. 특히 local-exec나 remote-exec 같은 provisioner 블럭을 통해 사용자가 지정한 Shell Script를 실행할 수 있다. 이를 통해 Filesystem에 위치한 Manifest 파일(.yaml)을 kubectl 명령어로 배포할 수 있다. 아래 예시는 kubectl이 이미 설치되었고 kubeconfig 역시 <Home Directory>/.kube/config 파일에 위치한 상황을 가정한다.

resource "kubenetes_namespace" "test" {
  metadata {
    name = "test"
  }
}

resource "null_resource" "k8s_cluster_bootstrap" {
  provisioner "local-exec" {
    command = "kubectl apply -n ${kubernetes_namespace.test.metadata[0].name} -f /tmp/nginx.yaml"
  }
  depends_on = [
    kubernetes_namespace.test,
  ]
}
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80

이 방식은 어떠한 K8s 리소스라도 쉽고 빠르게 생성할 수 있다는 장점이 있지만, 아래와 같은 단점 역시 있기 때문에 사용에 유의해야 한다.

  • Shell Script나 명령어를 실행하는 방식이므로 선언형 프로그래밍(Delclarative Programming) 방식의 IaC 철학(Idempotency, Immutability)와는  다소 거리가 있다.
  • Terraform 실행환경이 K8s API Server에 접근할 수 없는 경우(ex: Private Cluster), 사용이 어렵다.

Option 4. kubectl Provider 활용

기존 Manifest 파일을 그대로 사용할 수 있으면서, Terraform의 리소스로 관리할 수 있는 방식이다. 우선 아래와 같이 kubectl Provider를 선언한다. 기존 kubernetes, helm과 동일하게 kubeconfig, 인증서 방식 모두 사용 가능하다.

terraform {
    required_version = ">=1.2.2"
    
    required_providers {
        kubernetes = {
            source  = "hashicorp/kubernetes"
            version = ">= 2.0.0"
        }
        kubectl = {
            source  = "gavinbunney/kubectl"
            version = ">= 1.14.0"
        }
    }
}

provider "kubernetes" {
  host = "https://cluster_endpoint:port"

  client_certificate     = file("~/.kube/client-cert.pem")
  client_key             = file("~/.kube/client-key.pem")
  cluster_ca_certificate = file("~/.kube/cluster-ca-cert.pem")
}

provider "kubectl" {
    load_config_file       = false
    host     = "https://cluster_endpoint:port"
    client_certificate     = file("~/.kube/client-cert.pem")
    client_key             = file("~/.kube/client-key.pem")
    cluster_ca_certificate = file("~/.kube/cluster-ca-cert.pem"))
}

 

다음은 하나의 디렉토리에 존재하는 여러개의 YAML 파일로부터 K8s 리소스들을 생성하는 예시이다. 기존 kubernetes_namespace 리소스와 같이 사용할 수 있다.

resource "kubernetes_namespace" "test" {
    metadata {
        name = "test"
    }
}

data "kubectl_filename_list" "test_yamls" {
    pattern = "${path.module}/test/*.yaml"
}

resource "kubectl_manifest" "test_manifest" {
    count = length(data.kubectl_filename_list.test_yamls.matches)
    yaml_body = file(element(data.kubectl_filename_list.test_yamls.matches, count.index))
    override_namespace = kubernetes_namespace.test.metadata[0].name
}

 

YAML 파일을 그대로 사용할 수 있다는 점에서 매우 유용하지만 Hashicorp에서 제공하는 Provider가 아니기 때문에, 앞으로도 지속적인 Maintain이 이루어질지 약간의 우려가 있다.

 

 

개인적인 의견

Terraform은 인프라를 관리하기 위한 최적의 툴 중 하나이며, 클러스터 최초 생성시 기본 제공해야하는 시스템 애플리케이션을 Bootstrap형식으로 설치하기 매우 용이하다. 다만 실제 서비스를 위한 애플리케이션 배포는 엄연히 CI/CD 파이프라인에서 이루어지는 프로세스이며, 이미 Jenkins,  Argo CD와 같이 특화된 S/W가 존재하기 때문에 이 부분까지 Terraform을 사용할 필요는 없음에 유의하자.

 

 

참고

Kubernetes Provider for Terraform

 

'IaC' 카테고리의 다른 글

Terraform - Azure(3)  (0) 2022.03.14
Terraform - Azure(2)  (0) 2022.03.13
Terraform - Azure(1)  (0) 2022.03.11
Terraform - (7) Advanced  (1) 2021.06.19
Terraform - (6) IAM  (0) 2021.06.18