IaC 관리 구조
Layer 구조
Terragrunt Live
├ dev
│ ├ infra
│ ├ platform
├ stg
├ prod
Terraform Modules
├ network
├ bastion
├ nks
├ database
├ storage
├ cert
Infrastructure (NCP)
├ VPC / Subnet / NAT
├ Bastion
├ NKS
├ Cloud DB
├ Object Storage
Repo 구조
terragrunt repository
├ dev
│ ├ infra
│ │ ├ network
│ │ │ ├ terragrunt.hcl
modules
├ network
│ ├ main.tf
│ ├ variables.tf
│ ├ outputs.tf
IaC 구조는 개념적인 Layer 기준으로 먼저 설계했고, 실제 구현은 Terragrunt Repository와 Terraform Module Repository로 분리해서 구성했다. 아래는 실제 코드 기준 Repository 구조이다.
Network Module 구조
dev 환경 IaC 구축을 시작하면서 가장 먼저 구현한 영역은 Network Module이었다. Network 영역은 다른 리소스들이 의존하는 기반 인프라이기 때문에 가장 먼저 IaC로 구성했다.
modules/network
├ main.tf
├ variables.tf
├ outputs.tf
Network Module에는 아래 리소스들이 포함된다.
- VPC
- Subnet
- NAT Gateway
- Route Table
Network 인프라는 프로젝트 단위에서만 변경되는 경우가 대부분이고, 한번 생성되면 변경되지 않는 경우가 99% 이상이기 때문에 하나의 Module로 묶어서 관리하기로 했다.
작업 순서로는 Terraform Module을 먼저 구성하고 이후 Terragrunt에서 환경 단위로 조합하는 방식으로 진행했다.
실제로 작업하면서 Module 단위로 리소스를 먼저 정리하는 것이 dependency 연결이나 이후 구조 확장 시 훨씬 편했다.
Provider나 Backend 같은 공통 설정은 리소스 구조가 어느 정도 잡힌 이후 정리하는 것이 작업 흐름상 자연스러웠다.
variable.tf
network 리소스 생성을 위한 값을 입력받기 위한 terraform
Subnet, NAT, Route Table 같은 리소스는 여러 속성이 항상 같이 움직였기 때문에 개별 변수로 분리하는 것보다 Object로 묶는 것이 관리하기 편했다. 환경이 늘어나도 변수 구조를 바꾸지 않고 값만 변경하면 되기 때문에 재사용성 측면에서도 더 유리했다.
variable "vpc" {
type = object({
name = string
cidr = string
})
}
variable "subnets" {
type = map(list(object({
name = string
cidr = string
zone = string
usage_type = string
})))
}
variable "nat_gateway" {
type = list(object({
name = string
zone = string
subnet_name = string
}))
}
variable "route_tables" {
type = list(object({
name = string
target_name = string
target_type = string
subnet_type = string # ← 이거 반드시 추가
subnet_names = list(string)
}))
}
variable "enable" {
type = object({
subnet = bool
nat = bool
route_table = bool
})
}
Network main.tf
실제 network 리소스 생성을 위한 terraform
리소스 간 Dependency가 자연스럽게 연결되도록 구성했다. Network 리소스는 대부분 같이 생성되고 같이 변경되기 때문에 Terraform 내부 Reference 구조를 최대한 단순하게 유지하려고 했다. enable Flag는 환경별로 리소스 구성을 다르게 가져가야 하는 경우가 있어서 실제 운영 기준으로 추가했다.
############### VPC
# VPC 생성
resource "ncloud_vpc" "this" {
name = var.vpc.name
ipv4_cidr_block = var.vpc.cidr
}
############### Subnet
# subnet 생성 파일
locals {
flat_subnets = flatten([
for group, items in var.subnets : [
for s in items : merge(s, {
group = group
})
]
])
}
resource "ncloud_subnet" "this" {
for_each = {
for s in local.flat_subnets : s.name => s
}
vpc_no = ncloud_vpc.this.id
network_acl_no = ncloud_vpc.this.default_network_acl_no
subnet = each.value.cidr
zone = each.value.zone
subnet_type = each.value.group == "public" ? "PUBLIC" : "PRIVATE"
name = each.value.name
usage_type = each.value.usage_type
}
############### route-table
resource "ncloud_nat_gateway" "this" {
count = var.enable.nat ? 1 : 0
vpc_no = ncloud_vpc.this.id
name = var.nat_gateway[count.index].name
zone = var.nat_gateway[count.index].zone
subnet_no = ncloud_subnet.this[
var.nat_gateway[count.index].subnet_name
].id
}
############### route-table
resource "ncloud_route_table" "this" {
for_each = {
for rt in var.route_tables : rt.name => rt
}
vpc_no = ncloud_vpc.this.id
name = each.value.name
supported_subnet_type = each.value.subnet_type
}
resource "ncloud_route" "nat" {
for_each = {
for rt in var.route_tables :
rt.name => rt
if var.enable.route_table && var.enable.nat && rt.target_type == "NATGW"
}
route_table_no = ncloud_route_table.this[each.key].id
destination_cidr_block = "0.0.0.0/0"
target_name = ncloud_nat_gateway.this[0].name
target_type = "NATGW"
target_no = ncloud_nat_gateway.this[0].id
}
resource "ncloud_route_table_association" "this" {
for_each = {
for pair in flatten([
for rt in var.route_tables : [
for sn in rt.subnet_names : {
key = "${rt.name}-${sn}"
rt_name = rt.name
sn_name = sn
}
]
]) : pair.key => pair
}
route_table_no = ncloud_route_table.this[each.value.rt_name].id
subnet_no = ncloud_subnet.this[each.value.sn_name].id
}
Network output.tf
실제 network 리소스 생성후 결과물을 받기위한 terraform
다른 Module에서 실제로 사용할 가능성이 높은 값만 Output으로 제공했다.
Output이 많아질수록 Module 간 결합도가 높아지기 때문에 최소한으로 유지했다.
특히 Subnet은 Map 형태로 제공하는 것이 가독성 및 코드 이해도 때문에 편했다.
output "vpc_no" {
value = ncloud_vpc.this.id
}
output "subnet_ids" {
value = {
for k, s in ncloud_subnet.this : k => s.id
}
}
output "nat_gateway_no" {
value = var.enable.nat ? ncloud_nat_gateway.this[0].id : null
}
Dev Network Terragrunt
Dev 환경에서는 Terraform Module을 직접 호출하는 대신 Terragrunt를 사용해서 환경 설정과 공통 설정을 분리했다.
실제로 작업하면서 Provider, Backend, Version 같은 공통 설정을 환경마다 반복해서 관리하는 것이 비효율적이라고 느꼈고 root에서는 공통 설정을 관리하고 env에서는 환경별 값만 관리하도록 구조를 나눴다.
include "root" {
path = find_in_parent_folders("root.hcl")
expose = true
}
include "env" {
path = find_in_parent_folders("env.hcl")
expose = true
}
include "backend" {
path = find_in_parent_folders("_infra/backend.hcl")
}
include "provider" {
path = find_in_parent_folders("_infra/provider.hcl")
}
include "versions" {
path = find_in_parent_folders("_infra/versions.hcl")
}
terraform {
source = "${include.root.locals.root}/modules/network"
}
inputs = {
vpc = include.env.locals.vpc
subnets = include.env.locals.subnets
nat_gateway = include.env.locals.nat_gateway
route_tables = include.env.locals.route_tables
enable = include.env.locals.enable
}
root.hcl
root.hcl 에는 프로젝트 전체 환경에서 사용할 값을 넣도록 구성했다.
# 프로젝트 전역 공통 값
# live/root.hcl
locals {
root = get_repo_root()
project = "tf-test-public"
region = "KR"
zone = "KR-1"
# 현재는 임시로 넣어놓은거고 나중에 credential 이나 환경변수로 빼야함
site = "public" # 민간(public), 공공(gov), fin(fin)
ncloud_profile = "tf-test-public-dev" # s3 bucket 설정
# base state dir
site_endpoints = {
public = {
object_storage = "https://kr.object.ncloudstorage.com"
api_base = "https://api.ncloud.com"
monitoring = "https://monitoring.ncloud.com"
}
gov = {
object_storage = "https://kr.object.gov-ncloudstorage.com"
api_base = "https://api.gov-ncloud.com"
monitoring = "https://monitoring.gov-ncloud.com"
}
}
endpoints = local.site_endpoints[local.site]
# init script 기본값
bastion_defaults = {
admin_user = "leedh"
admin_password = "test1"
prom_password = "test1"
}
}
env.hcl
env에서는 실제 리소스 값만 관리하도록 분리했다. 이렇게 구성하니까 환경이 늘어나더라도 코드 수정 없이 Env 값만 추가하면 되는 구조가 만들어졌다.
locals {
env = "dev"
# terraform 디렉토리 루트
terraform_root = "${dirname(find_in_parent_folders("root.hcl"))}/../.."
root_cfg = read_terragrunt_config(find_in_parent_folders("root.hcl"))
project = local.root_cfg.locals.project
############################## 실제 수정 수정 영역 #############################
state_bucket = "tf-test-public-tfstate" # 이건 수동으로 만들거나, bootstrap 작업필요
enable = {
subnet = true
nat = true
route_table = true
}
############################## VPC #############################
vpc = {
name = "tf-dev-vpc"
cidr = "10.10.0.0/16"
}
############################## subnets #############################
subnets = {
public = [
{
name = "tf-dev-pub-a"
cidr = "10.10.1.0/24"
zone = "KR-1"
usage_type = "GEN"
},
{
name = "tf-dev-pub-nat-a"
cidr = "10.10.2.0/24"
zone = "KR-1"
usage_type = "NATGW"
},
{
name = "tf-dev-pub-lb-a"
cidr = "10.10.3.0/24"
zone = "KR-1"
usage_type = "LOADB"
}
]
private = [
{
name = "tf-dev-pri-a"
cidr = "10.10.4.0/24"
zone = "KR-1"
usage_type = "GEN"
},
{
name = "tf-dev-pri-lb-a"
cidr = "10.10.5.0/24"
zone = "KR-1"
usage_type = "LOADB"
}
]
}
############################## nat gateway #############################
nat_gateway = [
{
name = "tf-dev-nat-gw"
zone = "KR-1"
# 어떤 public subnet에 붙일지 인덱스
subnet_name = "tf-dev-pub-nat-a"
}
]
############################## route table #############################
route_tables = [
{
name = "tf-dev-pri-nat-rt"
target_name = "tf-dev-nat-gw"
target_type = "NATGW"
# 어떤 private subnet들을 연결할지 (이름 기준)
subnet_names = [
"tf-dev-pri-lb-a",
"tf-dev-pri-a"
]
subnet_type = "PRIVATE"
}
]
}
'infra > terraform' 카테고리의 다른 글
| IaC 전체 프로세스 설계 (0) | 2026.02.11 |
|---|---|
| Terraform, Terragrunt (0) | 2026.02.11 |
| Terraform 도입 이유 (0) | 2026.02.11 |