Skip to main content

OKE 플랫폼 엔지니어링 랩 1편 — Terraform + Terragrunt로 OKE 클러스터 프로비저닝하기

3 min 455 words

이 포스트는 OKE 플랫폼 엔지니어링 랩 시리즈의 1편입니다.


1. 배경

Oracle Cloud Free Tier에서는 ARM 기반 VM.Standard.A1.Flex 인스턴스를 월 3,000 OCPU 시간, 18GB RAM까지 무료로 사용할 수 있습니다. 여기에 OKE(Oracle Kubernetes Engine) 관리형 클러스터를 올리면 사실상 무료로 Kubernetes 환경을 운영할 수 있습니다.

인프라를 코드로 관리하기 위해 Terraform을 사용했고, 환경이 늘어날 때 반복 코드가 생기는 문제를 해결하기 위해 Terragrunt를 함께 사용했습니다.

Terraform만 사용하면 환경별(dev, prod)로 backend 설정과 provider 블록을 매번 복붙해야 합니다. Terragrunt는 이 공통 설정을 root.hcl 한 곳에 정의하고 각 모듈이 상속받는 구조를 만들어줍니다.


2. 디렉터리 구조

oke-cluster/
├── live/                         # Terragrunt 실행 환경
│   ├── root.hcl                  # remote state, provider 공통 정의
│   ├── tenancy.hcl               # OCI 인증 정보
│   └── dev/
│       ├── env.hcl
│       ├── vcn/
│       │   └── terragrunt.hcl
│       └── oke/
│           └── terragrunt.hcl
└── modules/                      # Terraform 모듈
    ├── vcn/
    │   └── main.tf, variables.tf, outputs.tf
    └── oke/
        └── main.tf, variables.tf, outputs.tf
        # 클러스터, 노드풀, cloud-init NFS

live/는 Terragrunt 실행 환경이고, modules/는 실제 리소스를 정의하는 Terraform 코드입니다. 환경을 추가할 때는 live/prod/ 같은 디렉터리만 만들고 env.hcl 값만 바꾸면 됩니다.


3. Terragrunt DRY 구조

root.hcl — 공통 설정

root.hcl에 remote state와 OCI provider를 한 번만 정의합니다. 모든 모듈이 include "root"로 이 파일을 상속받습니다.

# live/root.hcl
locals {
  tenancy = read_terragrunt_config(find_in_parent_folders("tenancy.hcl"))
  env     = read_terragrunt_config(find_in_parent_folders("env.hcl"))
}

remote_state {
  backend = "oci"
  config = {
    bucket    = "tfstate-${local.env.locals.env_name}"
    namespace = local.tenancy.locals.object_namespace
    key       = "${path_relative_to_include()}/terraform.tfstate"
    region    = local.env.locals.oci_region
    auth      = "APIKey"
    # ...인증 정보
  }
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite"
  }
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite"
  contents  = <<EOF
terraform {
  required_providers {
    oci = {
      source  = "oracle/oci"
      version = "~> 6.0"
    }
  }
}
provider "oci" {
  region       = "${local.env.locals.oci_region}"
  tenancy_ocid = "${local.tenancy.locals.tenancy_ocid}"
  # ...
}
EOF
}

path_relative_to_include()를 사용하면 각 모듈의 state 파일 경로가 자동으로 dev/vcn/terraform.tfstate, dev/oke/terraform.tfstate처럼 분리됩니다.

env.hcl — 환경 변수

환경별로 달라지는 값만 env.hcl에 모아둡니다.

# live/dev/env.hcl
locals {
  env_name           = "dev"
  oci_region         = "ap-chuncheon-1"
  compartment_id     = "<your-compartment-ocid>"
  vcn_cidr           = "10.0.0.0/16"
  kubernetes_version = "1.34.2"
  ssh_public_key     = "<your-ssh-public-key>"
}

dependency 블록 — 모듈 간 의존성

oke 모듈은 vcn 모듈이 만든 서브넷 OCID를 참조해야 합니다. Terragrunt의 dependency 블록을 사용하면 vcn의 output을 oke에서 직접 참조할 수 있고, 프로비저닝 순서도 자동으로 보장됩니다.

# live/dev/oke/terragrunt.hcl
include "root" {
  path = find_in_parent_folders("root.hcl")
}

locals {
  env = read_terragrunt_config(find_in_parent_folders("env.hcl"))
}

dependency "vcn" {
  config_path = "../vcn"

  # plan/validate 시 실제 VCN 없이도 동작하도록 mock 값 설정
  mock_outputs = {
    vcn_id              = "ocid1.vcn.mock"
    private_subnet_id   = "ocid1.subnet.mock.private"
    public_subnet_id    = "ocid1.subnet.mock.public"
    public_lb_subnet_id = "ocid1.subnet.mock.lb"
  }
  mock_outputs_allowed_terraform_commands = ["validate", "plan"]
}

terraform {
  source = "../../../modules/oke"
}

inputs = {
  compartment_id     = local.env.locals.compartment_id
  kubernetes_version = "v${local.env.locals.kubernetes_version}"
  ssh_public_key     = local.env.locals.ssh_public_key
  cluster_name       = "${local.env.locals.env_name}-oke-cluster"

  vcn_id              = dependency.vcn.outputs.vcn_id
  private_subnet_id   = dependency.vcn.outputs.private_subnet_id
  public_subnet_id    = dependency.vcn.outputs.public_subnet_id
  public_lb_subnet_id = dependency.vcn.outputs.public_lb_subnet_id
}

mock_outputs_allowed_terraform_commands = ["validate", "plan"] 덕분에 실제 VCN이 없는 상태에서도 terragrunt plan이 가능합니다. CI 환경에서 드라이런을 할 때 유용합니다.


4. VCN 구성

VCN은 oracle-terraform-modules/vcn/oci 커뮤니티 모듈을 사용해 인터넷 게이트웨이, NAT 게이트웨이, 서비스 게이트웨이를 한 번에 생성합니다.

# modules/vcn/main.tf
module "vcn" {
  source = "oracle-terraform-modules/vcn/oci"

  compartment_id = var.compartment_id
  region         = var.region
  vcn_name       = var.vcn_name
  vcn_dns_label  = var.vcn_dns_label
  vcn_cidrs      = [var.vcn_cidr]

  create_internet_gateway = true
  create_nat_gateway      = true
  create_service_gateway  = true
}

서브넷

cidrsubnet() 함수로 VCN CIDR(10.0.0.0/16)을 /24로 분할합니다.

# Public LB Subnet — 10.0.0.0/24 (Kubernetes API, LoadBalancer)
resource "oci_core_subnet" "public_lb_subnet" {
  cidr_block     = cidrsubnet(var.vcn_cidr, 8, 0)
  route_table_id = module.vcn.ig_route_id   # 인터넷 게이트웨이 라우팅
  # ...
}

# Public Subnet — 10.0.1.0/24 (공개 노드풀)
resource "oci_core_subnet" "public_subnet" {
  cidr_block     = cidrsubnet(var.vcn_cidr, 8, 1)
  route_table_id = module.vcn.ig_route_id
  # ...
}

# Private Subnet — 10.0.2.0/24 (프라이빗 노드풀)
resource "oci_core_subnet" "private_subnet" {
  cidr_block                 = cidrsubnet(var.vcn_cidr, 8, 2)
  route_table_id             = module.vcn.nat_route_id  # NAT 게이트웨이 라우팅
  prohibit_public_ip_on_vnic = true
  # ...
}

보안 리스트

각 서브넷에 Security List를 붙여 필요한 포트만 허용합니다.

서브넷허용 인바운드
Public LB6443 (K8s API), 443, 80
Public22 (SSH), VCN 내부 전체
PrivateVCN 내부 전체만

5. OKE 클러스터

노드 이미지 동적 조회

노드 이미지 ID를 하드코딩하면 Kubernetes 버전을 올릴 때마다 ID를 직접 찾아야 합니다. data source로 조회해 Kubernetes 버전에 맞는 이미지를 자동으로 선택했습니다.

# modules/oke/main.tf
data "oci_containerengine_node_pool_option" "oke_node_pool_option" {
  node_pool_option_id = "all"
}

locals {
  k8s_version_stripped = trim(var.kubernetes_version, "v")

  # Oracle Linux 8 aarch64 OKE 이미지 중 버전에 맞는 것 선택
  workers_image_id = [
    for source in data.oci_containerengine_node_pool_option.oke_node_pool_option.sources :
    source.image_id
    if(
      source.source_type == "IMAGE" &&
      can(regex("Oracle-Linux-8.*aarch64.*OKE-${local.k8s_version_stripped}", source.source_name))
    )
  ][0]
}

클러스터 생성

resource "oci_containerengine_cluster" "k8s_cluster" {
  compartment_id     = var.compartment_id
  kubernetes_version = local.k8s_version_with_v
  name               = var.cluster_name
  vcn_id             = var.vcn_id

  endpoint_config {
    is_public_ip_enabled = true
    subnet_id            = var.public_lb_subnet_id
  }

  options {
    kubernetes_network_config {
      pods_cidr     = "10.244.0.0/16"
      services_cidr = "10.96.0.0/16"
    }
    service_lb_subnet_ids = [var.public_lb_subnet_id]
  }

  cluster_pod_network_options {
    cni_type = "FLANNEL_OVERLAY"
  }
}

노드풀

공개/프라이빗 두 개의 노드풀을 생성합니다. 둘 다 OCI Free Tier에서 사용 가능한 ARM 기반 VM.Standard.A1.Flex를 사용합니다.

노드풀vCPURAM부트볼륨서브넷
Public212GB150GBPublic Subnet
Private212GB50GBPrivate Subnet

공개 노드풀의 부트볼륨이 더 큰 이유는 NFS 서버용 스토리지를 함께 담당하기 때문입니다.


6. NFS 서버 (cloud-init)

별도 스토리지 서비스 없이 공개 노드풀 VM에 cloud-init으로 NFS 서버를 직접 구성합니다. OKE 클러스터의 nfs-subdir-external-provisioner가 이 NFS를 백엔드로 동적 PV를 프로비저닝하는 방식입니다.

cloud-init 스크립트는 노드풀 생성 시 user_data로 주입합니다.

locals {
  public_node_cloudinit = base64encode(<<-EOF
    #!/bin/bash
    set -euo pipefail

    # OKE 초기화 스크립트 실행 (kubelet 설정 등)
    curl --fail -H "Authorization: Bearer Oracle" -L0 \
      http://169.254.169.254/opc/v2/instance/metadata/oke_init_script \
      | base64 --decode > /var/run/oke-init.sh
    bash /var/run/oke-init.sh

    # 디스크 확장
    /usr/libexec/oci-growfs -y

    # NFS 서버 설치 및 구성
    dnf install -y nfs-utils
    mkdir -p /exports/nfs
    chown nobody:nobody /exports/nfs
    chmod 777 /exports/nfs

    # VCN 내부(10.0.0.0/16) 전체에 읽기/쓰기 허용
    echo "/exports/nfs 10.0.0.0/16(rw,sync,no_subtree_check,no_root_squash)" > /etc/exports
    systemctl enable --now nfs-server rpcbind

    # firewalld NFS 포트 허용
    if systemctl is-active --quiet firewalld; then
      firewall-cmd --permanent --add-service=nfs
      firewall-cmd --permanent --add-service=rpc-bind
      firewall-cmd --permanent --add-service=mountd
      firewall-cmd --reload
    fi

    exportfs -rav
    systemctl restart kubelet.service
  EOF
  )
}

resource "oci_containerengine_node_pool" "public_node_pool" {
  # ...
  node_metadata = {
    user_data = local.public_node_cloudinit
  }
  # ...
}

OKE 노드는 Oracle Linux 기반이라 부팅 시 OKE 초기화 스크립트(oke_init_script)를 먼저 실행해야 합니다. NFS 설정은 그 이후에 진행하고, 마지막에 kubelet을 재시작해 노드가 클러스터에 정상 등록되도록 합니다.


7. 실행

# VCN 먼저 생성
cd live/dev/vcn
terragrunt apply

# OKE 클러스터 생성 (VCN output 자동 참조)
cd live/dev/oke
terragrunt apply

# 또는 한 번에 전체 실행
cd live/dev
terragrunt run-all apply

run-all applydependency 블록을 기반으로 실행 순서를 자동으로 결정합니다. vcn이 완료된 뒤 oke가 실행됩니다.

클러스터 생성 후 kubeconfig를 설정합니다.

oci ce cluster create-kubeconfig \
  --cluster-id <cluster-ocid> \
  --file ~/.kube/config \
  --region ap-chuncheon-1 \
  --token-version 2.0.0