OKE 플랫폼 엔지니어링 랩 1편 — Terraform + Terragrunt로 OKE 클러스터 프로비저닝하기
이 포스트는 OKE 플랫폼 엔지니어링 랩 시리즈의 1편입니다.
- 1편 — Terraform + Terragrunt로 OKE 클러스터 프로비저닝하기 ← 현재
- 2편 — ArgoCD App-in-Apps로 인프라 배포하기
- 3편 — Vault + External Secrets로 Kubernetes 시크릿 관리하기
- 4편 — Istio + cert-manager로 TLS 인증서 자동화하기
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 LB | 6443 (K8s API), 443, 80 |
| Public | 22 (SSH), VCN 내부 전체 |
| Private | VCN 내부 전체만 |
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를 사용합니다.
| 노드풀 | vCPU | RAM | 부트볼륨 | 서브넷 |
|---|---|---|---|---|
| Public | 2 | 12GB | 150GB | Public Subnet |
| Private | 2 | 12GB | 50GB | Private 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 apply는 dependency 블록을 기반으로 실행 순서를 자동으로 결정합니다. vcn이 완료된 뒤 oke가 실행됩니다.
클러스터 생성 후 kubeconfig를 설정합니다.
oci ce cluster create-kubeconfig \
--cluster-id <cluster-ocid> \
--file ~/.kube/config \
--region ap-chuncheon-1 \
--token-version 2.0.0