Skip to main content
Mandacode mandacode
홈 클러스터 구축하기 2: Terraform을 이용한 클러스터 구성 자동화
·
Kubernetes Proxmox VE Talos Linux Terraform Terragrunt

홈 클러스터 구축하기 2: Terraform을 이용한 클러스터 구성 자동화

Proxmox VE위에서 Terraform으로 VM생성부터 클러스터 구축까지

지난번 홈 클러스터 하드웨어 선택과 구성 글에 이어서, 이번에는 Terraform을 이용해 Proxmox VE 노드에 클러스터 구성을 자동화하는 과정을 다뤄보겠습니다.

Terraform 이란?

Terraform은 인프라를 코드로 관리할 수 있게 해주는 IaC(Infrastructure as Code) 도구입니다.

이전에 AWS 환경이나 홈랩을 운영하며 인프라를 직접 관리했던 경험이 있습니다. 서버를 운영하기 위해 수동으로 쉘에 접근해 필요한 종속성과 인프라를 구성하고, AWS의 경우 콘솔에서 인스턴스 사양을 일일이 조정하는 등 수동 작업에 많은 시간을 쏟았습니다.

이후 Terraform을 접하면서 자동화와 편의성을 극대화할 수 있는 도구라고 판단했습니다. 특히 쿠버네티스 클러스터나 Proxmox VE 같은 하이퍼바이저, AWS 같은 클라우드 인프라를 관리할 때 대시보드에서 직접 통제하는 방식보다 코드로 인프라를 관리하는 편이 확장성 측면에서 훨씬 유리했고, 향후 규모가 크거나 복잡한 인프라를 구성할 때 변경 내역을 추적하거나 반복 작업을 피할 수 있다는 장점도 컸습니다.

intro-terraform-workflow

(Terraform workflow)

Terraform은 크게 3단계로 구성됩니다.

  1. Write: 설정 파일로 인프라 구성 요소를 정의합니다.

  2. Plan: 현재 인프라 구성과 비교하여 변경 사항을 확인합니다.

  3. Apply: 인프라에 실제로 설정을 적용하고 state 파일을 업데이트합니다.

선언적으로 인프라가 정의되고 관리되어 일관적이고 편리한 인프라 구성이 가능해집니다. 또한 모듈 구조를 사용하여 반복적인 작업 없이 구성이 가능해집니다.

$ tree minimal-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf

$ tree complete-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── ...
├── modules/
│   ├── nestedA/
│   │   ├── README.md
│   │   ├── variables.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   ├── nestedB/
│   ├── .../
├── examples/
│   ├── exampleA/
│   │   ├── main.tf
│   ├── exampleB/
│   ├── .../

(Terraform 모듈 구조)

Terragrunt 란?

Terraform은 그 자체로 편리하게 인프라를 구성할 수 있도록 해주는 매우 좋은 도구입니다. 하지만 여러 배포 환경별로 코드를 반복해서 작성해야하고, 선언적으로 작성되는 도구이기 때문에 컴포넌트간 의존성이 있는 경우 문제가 복잡해질 수 있습니다. Terragrunt는 Terraform의 wrapper 도구로 위의 문제들을 해결해 줍니다.

Unit 구조

유닛이란 terragrunt.hcl 파일을 포함하는 디렉토리를 의미하며 Terragrunt에서 배포 가능한 가장 작은 단위입니다.

terraform {
  # Deploy version v0.0.3 in stage
  source = "git::git@github.com:foo/modules.git//app?ref=v0.0.3"
}

inputs = {
  instance_count = 3
  instance_type  = "t4g.micro"
}

위와 같이 선언되어 미리 작성된 모듈에 입력 값을 넣을 수 있도록 구성되어 있습니다.

Includes

여러 환경에서 반복적인 Provider 설정을 하는 대신, 환경별 또는 공통적으로 설정할 수 있도록 구성하는 방법입니다.

일반적으로 루트에 root.hcl 을 만들어 전역적으로 설정을 만듭니다.

# root.hcl
remote_state {
  backend = "s3"
  config = {
    bucket         = "my-tofu-state"
    key            = "${path_relative_to_include()}/tofu.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "my-lock-table"
  }
}

generate "provider" {
  path = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents = <<EOF
provider "aws" {
  assume_role {
    role_arn = "arn:aws:iam::0123456789:role/terragrunt"
  }
}
EOF
}

# app/terragrunt.hcl
include "root" {
  path = find_in_parent_folders("root.hcl")
}

위의 구조 뿐만 아니라 root.hcl과 같은 파일을 환경별로(prod/env) 선언하여 환경별로 설정을 하는 방법도 있습니다.

include "root" {
  path = find_in_parent_folders("root.hcl")
}

include "k8s-providers" {
  path = find_in_parent_folders("k8s-providers.hcl")
}

# Storage needs cert-manager for CSI webhook validation
dependency "security" {
  config_path  = "../05_security"
  skip_outputs = true
}

terraform {
  source = "${get_repo_root()}/modules/rook-ceph"
}

inputs = {
  kubeconfig_path = "${get_repo_root()}/.cache/production/kubeconfig"
}

State Backend

Terraform에서 state는 로컬에서 관리되어 작업 환경 또는 여러 팀에서 관리하기 부적합 할 수 있습니다. State Backend를 이용하면 S3와 같은 외부 스토리지와 연동하여 원격으로 State를 관리할 수 있고 변수나 함수, 표현식을 지원하여 안정적이고 일관적인 State 관리가 가능해집니다.

remote_state {
  backend = "s3"
  config = {
    bucket       = "my-tofu-state"
    key          = "${path_relative_to_include()}/tofu.tfstate"
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true
  }
}

인프라 구성 적용

현재 구성중인 홈 클러스터는 Proxmox 위에 여러 가상머신들을 생성하고 Talos Linux를 이용하여 구성된 쿠버네티스 클러스터입니다.

여러 가상머신을 사용하기 때문에 자동화 및 일관성이 있어야 했고 위와 같은 IaC 도구를 사용했을 때 이 부분이 해결될 수 있었습니다.

이 때, 향후 ArgoCD를 통해 서비스들이 배포될 예정이었고, 별도의 프로젝트로 구성하고자 했기 때문에 인프라의 영역와 서비스 영역을 다음과 같이 결정하였습니다.

  • 인프라 영역 - 클러스터 운영을 위한 필요 구성

  • 서비스 영역 - 구성된 클러스터 위에서 동작하는 서비스

모듈 구조

가상머신, CNI, Storage(Ceph), Secret Manager, ArgoCD 등 클러스터 위에서 서비스들을 동작시키기 위해 필요한 것들을 인프라 영역으로 보았고 다음과 같은 Terraform 모듈을 구성하였습니다.

> tree -L 1 modules/
modules/
├── bootstrap
├── cluster
├── gitops
├── network
├── secrets
└── storage

> cat modules/network/main.tf                                                                                                                                                                    
resource "helm_release" "cilium" {
  name       = "cilium"
  repository = "https://helm.cilium.io/"
  chart      = "cilium"
  version    = var.cilium_version
  namespace  = "kube-system"

  wait          = true
  wait_for_jobs = true
  timeout       = 600

  set = [
    {
      name  = "kubeProxyReplacement"
      value = "true"
    },
    {
      name  = "k8sServiceHost"
      value = "localhost"
    },
    {
      name  = "k8sServicePort"
      value = "7445"
    },
    {
      name  = "ipam.mode"
      value = "kubernetes"
    },
    {
      name  = "cgroup.autoMount.enabled"
      value = "false"
    },
    {
      name  = "cgroup.hostRoot"
      value = "/sys/fs/cgroup"
    },
...

> cat modules/network/outputs.tf                                                                                                                                                                 
output "cilium_status" {
  description = "Status metadata for the Cilium Helm release"
  value = {
    release   = helm_release.cilium.status
    version   = helm_release.cilium.version
    namespace = helm_release.cilium.namespace
  }
}

output "cilium_lb_pool_name" {
  description = "Name of the Cilium LoadBalancer IPPool"
  value       = kubectl_manifest.cilium_loadbalancer_ip_pool.name
}

output "cilium_l2_policy_name" {
  description = "Name of the Cilium L2 Announcement Policy"
  value       = kubectl_manifest.cilium_l2_announcement_policy.name
}

위와 같이 책임별로 각 모듈을 구성하였고, 결과를 다음 모듈에서 이용할 수 있도록 outputs.tf를 작성하였습니다.

Terragrunt 계층 구조

각 모듈에서 출력된 결과를 다음 모듈에 전달해주고, 전체 혹은 환경별로 설정을 지정하기 위해서는 Terragrunt가 필요합니다.

또한 이런 인프라 프로젝트에서 각 코드는 직관적이고 예측 가능하게 동작해야 하기 때문에 다음과 같이 순차적으로 적용될 수 있도록 폴더 구조 및 의존성을 설정하였습니다.

> tree -L 2 environments/                                                                                                                                                                        
environments/
└── production
    ├── 01_bootstrap
    ├── 02_cluster
    ├── 03_network
    ├── 04_storage
    ├── 05_secrets
    ├── 06_gitops
    ├── env.hcl
    └── k8s-providers.hcl

> cat environments/production/03_network/terragrunt.hcl                                                                                                                                          
include "root" {
  path = find_in_parent_folders("root.hcl")
}

include "k8s-providers" {
  path = find_in_parent_folders("k8s-providers.hcl")
}

# CNI must deploy after cluster is bootstrapped
dependency "cluster" {
  config_path = "../02_cluster"
  skip_outputs = true
}

terraform {
  source = "${get_repo_root()}/modules/network"
}

inputs = {
  kubeconfig_path = "${get_repo_root()}/.cache/production/kubeconfig"

  cilium_version = "1.19.4"
  cilium_lb_pool = "192.168.0.80-192.168.0.99"
}

마치며

이번 글에서는 Terraform및 Terragrunt를 이용하여 클러스터 구성 자동화를 한 방법에 대해 소개해 드렸습니다. 다음 글에서는 ArgoCD를 이용한 배포 전략에 대해서 다뤄보겠습니다.

또한 향후 Cilium CNI를 이용한 네트워크 구성 및 AWS OIDC를 연동한 방법에 대해서 추가로 설명드리겠습니다.

감사합니다 :)