- Published on
Terraform IaC 완전 가이드 2025: AWS/GCP 인프라를 코드로 관리하는 모든 것
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며
- 1. 왜 IaC가 중요한가
- 2. Terraform 기초
- 3. State 관리
- 4. 모듈 (Modules)
- 5. AWS 실전 인프라
- 6. GCP 실전 인프라
- 7. 고급 패턴
- 8. Import & 마이그레이션
- 9. Terragrunt
- 10. CI/CD 자동화
- 11. 비용 관리 (Infracost)
- 12. 보안 (tfsec, Checkov)
- 13. Terraform 자격증 가이드
- 14. 인터뷰 질문 15선
- 15. 퀴즈
- 참고 자료
들어가며
인프라를 수동으로 관리하는 시대는 끝났습니다. 클라우드 리소스가 수십, 수백 개로 늘어나면 콘솔 클릭으로는 재현성, 감사 추적, 협업이 불가능합니다. Terraform은 HashiCorp가 만든 오픈소스 IaC(Infrastructure as Code) 도구로, AWS, GCP, Azure를 비롯한 3,000개 이상의 프로바이더를 지원합니다.
이 가이드에서는 Terraform의 기초부터 실전까지 모든 것을 다룹니다. HCL 문법, State 관리, 모듈 설계, AWS/GCP 실전 인프라, Terragrunt, CI/CD 자동화, 보안 스캐닝, 비용 추정까지 — 프로덕션 레벨의 IaC를 구축하는 데 필요한 모든 지식을 체계적으로 정리합니다.
1. 왜 IaC가 중요한가
1.1 수동 관리 vs IaC
| 관점 | 수동 (콘솔) | IaC (Terraform) |
|---|---|---|
| 재현성 | 불가능 | 코드로 100% 재현 |
| 감사 추적 | 제한적 | Git 히스토리 |
| 협업 | 불가능 | PR 리뷰 |
| 환경 복제 | 수 시간 | 수 분 |
| 롤백 | 수동 복구 | terraform apply |
| 문서화 | 별도 관리 | 코드가 문서 |
1.2 Terraform vs 다른 IaC 도구
| 도구 | 언어 | 클라우드 | 상태 관리 | 학습 곡선 |
|---|---|---|---|---|
| Terraform | HCL | 멀티 클라우드 | State 파일 | 중간 |
| CloudFormation | JSON/YAML | AWS 전용 | 스택 | 낮음 |
| Pulumi | Python/TS/Go | 멀티 클라우드 | State 파일 | 높음 |
| AWS CDK | Python/TS/Go | AWS 전용 | CloudFormation | 높음 |
| OpenTofu | HCL | 멀티 클라우드 | State 파일 | 중간 |
Terraform을 선택하는 이유:
- 멀티 클라우드 지원 (AWS + GCP + Azure)
- 가장 큰 커뮤니티와 모듈 레지스트리
- 선언적 구문으로 의도 명확
- Plan -> Apply 워크플로로 안전한 변경
2. Terraform 기초
2.1 HCL 기본 문법
# 프로바이더 설정
terraform {
required_version = ">= 1.7.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.40"
}
}
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "terraform"
Project = var.project_name
}
}
}
2.2 변수 (Variables)
# variables.tf
variable "aws_region" {
description = "AWS region to deploy resources"
type = string
default = "ap-northeast-2"
}
variable "environment" {
description = "Environment name (dev, staging, prod)"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "instance_config" {
description = "EC2 instance configuration"
type = object({
instance_type = string
volume_size = number
enable_monitoring = bool
})
default = {
instance_type = "t3.medium"
volume_size = 50
enable_monitoring = true
}
}
variable "allowed_cidrs" {
description = "List of CIDR blocks allowed for ingress"
type = list(string)
default = ["10.0.0.0/8"]
}
variable "tags" {
description = "Additional tags"
type = map(string)
default = {}
}
2.3 리소스 (Resources)
# main.tf
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "main-vpc-${var.environment}"
}
}
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-${count.index + 1}"
Type = "public"
}
}
2.4 데이터 소스 (Data Sources)
# 최신 Amazon Linux 2023 AMI 조회
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# 현재 AWS 계정 정보
data "aws_caller_identity" "current" {}
# 현재 리전
data "aws_region" "current" {}
2.5 출력값 (Outputs)
# outputs.tf
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "Public subnet IDs"
value = aws_subnet.public[*].id
}
output "account_id" {
description = "AWS Account ID"
value = data.aws_caller_identity.current.account_id
sensitive = false
}
output "db_password" {
description = "Database password"
value = random_password.db.result
sensitive = true
}
2.6 Locals
locals {
name_prefix = "${var.project_name}-${var.environment}"
common_tags = merge(var.tags, {
Environment = var.environment
ManagedBy = "terraform"
Project = var.project_name
})
env_config = {
dev = {
instance_type = "t3.small"
min_size = 1
max_size = 2
}
staging = {
instance_type = "t3.medium"
min_size = 2
max_size = 4
}
prod = {
instance_type = "t3.large"
min_size = 3
max_size = 10
}
}
config = local.env_config[var.environment]
}
3. State 관리
3.1 State란?
Terraform State는 실제 인프라와 코드 간의 매핑 정보를 저장합니다. terraform.tfstate 파일에 모든 리소스의 현재 상태가 JSON으로 기록됩니다.
State가 없으면:
- Terraform이 어떤 리소스를 관리하는지 알 수 없음
- Plan에서 모든 리소스를 새로 생성하려고 함
- 리소스 간 의존성 추적 불가
3.2 원격 State (S3 + DynamoDB)
# backend.tf
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "environments/prod/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
S3 버킷과 DynamoDB 테이블 생성 (부트스트랩):
# bootstrap/main.tf
resource "aws_s3_bucket" "terraform_state" {
bucket = "mycompany-terraform-state"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
3.3 State 명령어
# State 목록 확인
terraform state list
# 특정 리소스 상세 정보
terraform state show aws_vpc.main
# 리소스 이름 변경 (코드 리팩터링 시)
terraform state mv aws_instance.old aws_instance.new
# State에서 리소스 제거 (실제 인프라는 유지)
terraform state rm aws_instance.legacy
# State 가져오기
terraform state pull > backup.tfstate
4. 모듈 (Modules)
4.1 모듈 생성
modules/
vpc/
main.tf
variables.tf
outputs.tf
ec2/
main.tf
variables.tf
outputs.tf
rds/
main.tf
variables.tf
outputs.tf
# modules/vpc/main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
enable_dns_support = true
enable_dns_hostnames = true
tags = merge(var.tags, {
Name = "${var.name_prefix}-vpc"
})
}
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = merge(var.tags, {
Name = "${var.name_prefix}-public-${count.index + 1}"
Type = "public"
})
}
resource "aws_subnet" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.private_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
tags = merge(var.tags, {
Name = "${var.name_prefix}-private-${count.index + 1}"
Type = "private"
})
}
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = merge(var.tags, {
Name = "${var.name_prefix}-igw"
})
}
resource "aws_nat_gateway" "this" {
count = var.enable_nat_gateway ? length(var.public_subnet_cidrs) : 0
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = merge(var.tags, {
Name = "${var.name_prefix}-nat-${count.index + 1}"
})
}
resource "aws_eip" "nat" {
count = var.enable_nat_gateway ? length(var.public_subnet_cidrs) : 0
domain = "vpc"
tags = merge(var.tags, {
Name = "${var.name_prefix}-eip-${count.index + 1}"
})
}
# modules/vpc/variables.tf
variable "name_prefix" {
description = "Name prefix for resources"
type = string
}
variable "cidr_block" {
description = "VPC CIDR block"
type = string
default = "10.0.0.0/16"
}
variable "public_subnet_cidrs" {
description = "Public subnet CIDR blocks"
type = list(string)
}
variable "private_subnet_cidrs" {
description = "Private subnet CIDR blocks"
type = list(string)
}
variable "availability_zones" {
description = "Availability zones"
type = list(string)
}
variable "enable_nat_gateway" {
description = "Enable NAT Gateway"
type = bool
default = true
}
variable "tags" {
description = "Additional tags"
type = map(string)
default = {}
}
# modules/vpc/outputs.tf
output "vpc_id" {
value = aws_vpc.this.id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
4.2 모듈 사용
# environments/prod/main.tf
module "vpc" {
source = "../../modules/vpc"
name_prefix = "prod"
cidr_block = "10.0.0.0/16"
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
availability_zones = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
enable_nat_gateway = true
tags = local.common_tags
}
module "rds" {
source = "../../modules/rds"
name_prefix = "prod"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
instance_class = "db.r6g.large"
engine_version = "15.5"
allocated_storage = 100
tags = local.common_tags
}
4.3 Terraform Registry 모듈 사용
# 공식 AWS VPC 모듈 사용
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.5.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = false
tags = local.common_tags
}
5. AWS 실전 인프라
5.1 VPC + EC2 + ALB + RDS
# EC2 인스턴스 + Auto Scaling Group
resource "aws_launch_template" "app" {
name_prefix = "${local.name_prefix}-app-"
image_id = data.aws_ami.amazon_linux.id
instance_type = local.config.instance_type
vpc_security_group_ids = [aws_security_group.app.id]
user_data = base64encode(templatefile("userdata.sh", {
environment = var.environment
db_host = aws_db_instance.main.endpoint
}))
iam_instance_profile {
name = aws_iam_instance_profile.app.name
}
block_device_mappings {
device_name = "/dev/xvda"
ebs {
volume_size = var.instance_config.volume_size
volume_type = "gp3"
encrypted = true
}
}
tag_specifications {
resource_type = "instance"
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-app"
})
}
}
resource "aws_autoscaling_group" "app" {
name = "${local.name_prefix}-app-asg"
desired_capacity = local.config.min_size
min_size = local.config.min_size
max_size = local.config.max_size
vpc_zone_identifier = module.vpc.private_subnet_ids
target_group_arns = [aws_lb_target_group.app.arn]
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
health_check_type = "ELB"
health_check_grace_period = 300
tag {
key = "Name"
value = "${local.name_prefix}-app"
propagate_at_launch = true
}
}
5.2 ALB 설정
resource "aws_lb" "app" {
name = "${local.name_prefix}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = module.vpc.public_subnet_ids
enable_deletion_protection = var.environment == "prod"
tags = local.common_tags
}
resource "aws_lb_target_group" "app" {
name = "${local.name_prefix}-app-tg"
port = 8080
protocol = "HTTP"
vpc_id = module.vpc.vpc_id
health_check {
path = "/actuator/health"
port = "traffic-port"
healthy_threshold = 3
unhealthy_threshold = 3
timeout = 5
interval = 30
matcher = "200"
}
deregistration_delay = 30
tags = local.common_tags
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.app.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate.main.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
5.3 RDS (PostgreSQL)
resource "aws_db_instance" "main" {
identifier = "${local.name_prefix}-db"
engine = "postgres"
engine_version = "15.5"
instance_class = "db.r6g.large"
allocated_storage = 100
max_allocated_storage = 500
storage_type = "gp3"
storage_encrypted = true
db_name = "myapp"
username = "admin"
password = random_password.db.result
multi_az = var.environment == "prod"
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.db.id]
backup_retention_period = 7
backup_window = "03:00-04:00"
maintenance_window = "Mon:04:00-Mon:05:00"
skip_final_snapshot = var.environment != "prod"
final_snapshot_identifier = "${local.name_prefix}-db-final"
performance_insights_enabled = true
tags = local.common_tags
}
resource "random_password" "db" {
length = 32
special = false
}
5.4 S3 + IAM
resource "aws_s3_bucket" "assets" {
bucket = "${local.name_prefix}-assets"
tags = local.common_tags
}
resource "aws_s3_bucket_versioning" "assets" {
bucket = aws_s3_bucket.assets.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_public_access_block" "assets" {
bucket = aws_s3_bucket.assets.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# IAM Role for EC2
resource "aws_iam_role" "app" {
name = "${local.name_prefix}-app-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
tags = local.common_tags
}
resource "aws_iam_role_policy" "app_s3" {
name = "${local.name_prefix}-app-s3-policy"
role = aws_iam_role.app.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
]
Resource = [
aws_s3_bucket.assets.arn,
"${aws_s3_bucket.assets.arn}/*"
]
}
]
})
}
6. GCP 실전 인프라
6.1 VPC + GKE
provider "google" {
project = var.gcp_project_id
region = var.gcp_region
}
resource "google_compute_network" "main" {
name = "${local.name_prefix}-vpc"
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "main" {
name = "${local.name_prefix}-subnet"
ip_cidr_range = "10.0.0.0/20"
region = var.gcp_region
network = google_compute_network.main.id
secondary_ip_range {
range_name = "pods"
ip_cidr_range = "10.1.0.0/16"
}
secondary_ip_range {
range_name = "services"
ip_cidr_range = "10.2.0.0/20"
}
}
resource "google_container_cluster" "primary" {
name = "${local.name_prefix}-gke"
location = var.gcp_region
remove_default_node_pool = true
initial_node_count = 1
network = google_compute_network.main.name
subnetwork = google_compute_subnetwork.main.name
ip_allocation_policy {
cluster_secondary_range_name = "pods"
services_secondary_range_name = "services"
}
workload_identity_config {
workload_pool = "${var.gcp_project_id}.svc.id.goog"
}
release_channel {
channel = "REGULAR"
}
}
resource "google_container_node_pool" "primary" {
name = "${local.name_prefix}-node-pool"
location = var.gcp_region
cluster = google_container_cluster.primary.name
node_count = 3
node_config {
machine_type = "e2-standard-4"
disk_size_gb = 100
disk_type = "pd-ssd"
oauth_scopes = [
"https://www.googleapis.com/auth/cloud-platform"
]
labels = {
environment = var.environment
}
}
autoscaling {
min_node_count = 3
max_node_count = 10
}
}
6.2 Cloud SQL + Cloud Run
resource "google_sql_database_instance" "main" {
name = "${local.name_prefix}-db"
database_version = "POSTGRES_15"
region = var.gcp_region
settings {
tier = "db-custom-4-16384"
ip_configuration {
ipv4_enabled = false
private_network = google_compute_network.main.id
}
backup_configuration {
enabled = true
point_in_time_recovery_enabled = true
start_time = "03:00"
}
availability_type = var.environment == "prod" ? "REGIONAL" : "ZONAL"
}
deletion_protection = var.environment == "prod"
}
resource "google_cloud_run_v2_service" "app" {
name = "${local.name_prefix}-app"
location = var.gcp_region
template {
containers {
image = "gcr.io/${var.gcp_project_id}/myapp:latest"
ports {
container_port = 8080
}
resources {
limits = {
cpu = "2"
memory = "1Gi"
}
}
env {
name = "SPRING_PROFILES_ACTIVE"
value = var.environment
}
startup_probe {
http_get {
path = "/actuator/health"
}
initial_delay_seconds = 10
period_seconds = 3
}
}
scaling {
min_instance_count = var.environment == "prod" ? 2 : 0
max_instance_count = 10
}
}
}
7. 고급 패턴
7.1 Workspaces
# 워크스페이스 생성
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
# 워크스페이스 전환
terraform workspace select prod
# 현재 워크스페이스 확인
terraform workspace show
# 워크스페이스 기반 환경 분리
locals {
env_config = {
dev = {
instance_type = "t3.small"
db_instance = "db.t3.medium"
}
staging = {
instance_type = "t3.medium"
db_instance = "db.r6g.large"
}
prod = {
instance_type = "t3.large"
db_instance = "db.r6g.xlarge"
}
}
config = local.env_config[terraform.workspace]
}
7.2 Dynamic Blocks
resource "aws_security_group" "app" {
name = "${local.name_prefix}-app-sg"
description = "Application security group"
vpc_id = module.vpc.vpc_id
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = ingress.value.cidr_blocks
description = ingress.value.description
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = local.common_tags
}
variable "ingress_rules" {
type = list(object({
port = number
cidr_blocks = list(string)
description = string
}))
default = [
{
port = 8080
cidr_blocks = ["10.0.0.0/8"]
description = "Application port"
},
{
port = 443
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS"
}
]
}
7.3 for_each vs count
# count -- 인덱스 기반 (삭제 시 재생성 문제)
resource "aws_subnet" "example" {
count = 3
cidr_block = "10.0.${count.index}.0/24"
vpc_id = aws_vpc.main.id
}
# for_each -- 키 기반 (안전한 삭제/추가)
resource "aws_subnet" "example" {
for_each = {
"public-a" = { cidr = "10.0.1.0/24", az = "ap-northeast-2a" }
"public-b" = { cidr = "10.0.2.0/24", az = "ap-northeast-2b" }
"private-a" = { cidr = "10.0.11.0/24", az = "ap-northeast-2a" }
}
cidr_block = each.value.cidr
availability_zone = each.value.az
vpc_id = aws_vpc.main.id
tags = {
Name = "${local.name_prefix}-${each.key}"
}
}
7.4 Lifecycle 규칙
resource "aws_instance" "app" {
ami = data.aws_ami.amazon_linux.id
instance_type = local.config.instance_type
lifecycle {
create_before_destroy = true
prevent_destroy = false
ignore_changes = [ami]
}
}
# moved 블록 (리소스 이름 변경 시)
moved {
from = aws_instance.old_name
to = aws_instance.new_name
}
8. Import & 마이그레이션
8.1 terraform import 명령어
# 기존 리소스 임포트
terraform import aws_vpc.main vpc-0123456789abcdef0
terraform import aws_instance.web i-0123456789abcdef0
terraform import 'aws_subnet.public[0]' subnet-0123456789abcdef0
8.2 Import Block (Terraform 1.5+)
# import.tf -- 선언적 임포트
import {
to = aws_vpc.main
id = "vpc-0123456789abcdef0"
}
import {
to = aws_instance.web
id = "i-0123456789abcdef0"
}
# 코드 자동 생성
terraform plan -generate-config-out=generated.tf
# 생성된 코드 검토 후 적용
terraform apply
8.3 기존 인프라 마이그레이션 전략
단계별 접근:
- 인벤토리 작성 — 현재 리소스 목록화
- 코드 작성 — 리소스별 HCL 코드 작성
- Import — 기존 리소스를 State에 가져오기
- Plan 검증 —
terraform plan으로 변경사항 없음 확인 - 점진적 관리 — 새 리소스부터 Terraform으로 생성
9. Terragrunt
9.1 DRY 환경 관리
infrastructure/
terragrunt.hcl
environments/
dev/
terragrunt.hcl
vpc/
terragrunt.hcl
rds/
terragrunt.hcl
prod/
terragrunt.hcl
vpc/
terragrunt.hcl
rds/
terragrunt.hcl
modules/
vpc/
rds/
# infrastructure/terragrunt.hcl (루트)
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
config = {
bucket = "mycompany-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
contents = <<EOF
provider "aws" {
region = "ap-northeast-2"
}
EOF
}
# infrastructure/environments/prod/vpc/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules/vpc"
}
inputs = {
name_prefix = "prod"
cidr_block = "10.0.0.0/16"
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"]
availability_zones = ["ap-northeast-2a", "ap-northeast-2b"]
enable_nat_gateway = true
}
9.2 의존성 관리
# infrastructure/environments/prod/rds/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules/rds"
}
dependency "vpc" {
config_path = "../vpc"
}
inputs = {
name_prefix = "prod"
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnet_ids
}
10. CI/CD 자동화
10.1 GitHub Actions + Terraform
# .github/workflows/terraform.yml
name: Terraform CI/CD
on:
pull_request:
paths:
- 'infrastructure/**'
push:
branches:
- main
paths:
- 'infrastructure/**'
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/terraform-ci
aws-region: ap-northeast-2
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.0
- name: Terraform Init
working-directory: infrastructure/environments/prod
run: terraform init
- name: Terraform Plan
working-directory: infrastructure/environments/prod
run: terraform plan -no-color -out=tfplan
apply:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/terraform-ci
aws-region: ap-northeast-2
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.0
- name: Terraform Init
working-directory: infrastructure/environments/prod
run: terraform init
- name: Terraform Apply
working-directory: infrastructure/environments/prod
run: terraform apply -auto-approve
10.2 Atlantis
# atlantis.yaml
version: 3
projects:
- name: prod-infra
dir: infrastructure/environments/prod
workspace: default
terraform_version: v1.7.0
autoplan:
when_modified:
- "*.tf"
- "../../modules/**/*.tf"
enabled: true
apply_requirements:
- approved
- mergeable
11. 비용 관리 (Infracost)
11.1 Infracost 통합
# Infracost 실행
infracost breakdown --path=infrastructure/environments/prod
# JSON 출력
infracost breakdown \
--path=infrastructure/environments/prod \
--format=json \
--out-file=infracost.json
# PR에 비용 코멘트
infracost comment github \
--path=infracost.json \
--repo=myorg/myrepo \
--pull-request=42 \
--github-token=GH_TOKEN_VALUE
11.2 태깅 전략
# 비용 추적을 위한 필수 태그
locals {
required_tags = {
Environment = var.environment
Project = var.project_name
Team = var.team_name
CostCenter = var.cost_center
ManagedBy = "terraform"
}
}
12. 보안 (tfsec, Checkov)
12.1 tfsec 정적 분석
# tfsec 실행
tfsec ./infrastructure/
# 특정 규칙 무시
tfsec ./infrastructure/ --exclude-downloaded-modules
# tfsec 규칙 무시 (정당한 사유가 있을 때만)
resource "aws_s3_bucket" "logs" {
bucket = "my-access-logs"
#tfsec:ignore:aws-s3-enable-versioning -- 로그 버킷은 버저닝 불필요
}
12.2 Checkov
# Checkov 실행
checkov -d ./infrastructure/ --framework terraform
# CI/CD에서 실행
checkov -d ./infrastructure/ \
--output junitxml \
--output-file-path ./results
12.3 Secrets 관리 (Vault)
# HashiCorp Vault에서 시크릿 조회
data "vault_generic_secret" "db" {
path = "secret/data/myapp/database"
}
resource "aws_db_instance" "main" {
username = data.vault_generic_secret.db.data["username"]
password = data.vault_generic_secret.db.data["password"]
}
13. Terraform 자격증 가이드
HashiCorp Certified: Terraform Associate (003)
시험 개요:
- 시간: 60분
- 문항: 57문항 (객관식 + 빈칸 채우기)
- 합격 기준: 70%
- 유효 기간: 2년
- 비용: 약 70 USD
핵심 도메인:
- IaC 개념 이해 (15%) — IaC 장점, Terraform 워크플로
- Terraform 기초 (20%) — 프로바이더, 리소스, 변수, 출력
- State 관리 (15%) — 원격 State, 잠금, 명령어
- 모듈 (15%) — 모듈 생성, 사용, 레지스트리
- Terraform CLI (15%) — init, plan, apply, destroy, import
- 워크플로 (10%) — 팀 워크플로, Terraform Cloud
- 구현 및 유지보수 (10%) — 디버깅, 업그레이드
학습 리소스:
- HashiCorp Learn 공식 튜토리얼
- Terraform Up and Running (O'Reilly)
- 공식 시험 리뷰 가이드
14. 인터뷰 질문 15선
기본 (1-5)
Q1. Terraform의 핵심 워크플로를 설명하세요.
Write(코드 작성) -> Plan(변경사항 미리보기) -> Apply(적용)의 3단계입니다. terraform plan은 실제 변경 없이 무엇이 바뀔지 보여주고, terraform apply가 실제 인프라를 변경합니다.
Q2. Terraform State의 역할과 중요성은?
State는 실제 인프라와 코드 간의 매핑을 저장합니다. State가 없으면 Terraform은 어떤 리소스를 관리하는지 알 수 없습니다. 원격 백엔드(S3+DynamoDB)를 사용하여 팀 공유와 잠금을 구현해야 합니다.
Q3. Provider와 Resource의 차이는?
Provider는 클라우드/서비스와의 API 연결을 담당하는 플러그인(AWS, GCP 등)이고, Resource는 Provider를 통해 관리하는 실제 인프라 객체(VPC, EC2 등)입니다.
Q4. terraform plan과 terraform apply의 차이는?
plan은 변경사항을 미리보기로 보여주며 실제 인프라를 변경하지 않습니다. apply는 실제로 인프라를 변경합니다. apply는 내부적으로 plan을 먼저 실행합니다.
Q5. 변수의 우선순위(Variable Precedence)를 설명하세요.
환경변수 TF_VAR_xxx, terraform.tfvars, terraform.tfvars.json, auto.tfvars, -var 또는 -var-file 명령줄 옵션 순으로 우선순위가 높아집니다.
심화 (6-10)
Q6. count와 for_each의 차이와 언제 어떤 것을 사용해야 하나요?
count는 인덱스 기반으로 중간 요소 삭제 시 뒤의 모든 리소스가 재생성됩니다. for_each는 키 기반으로 특정 요소만 안전하게 삭제/추가 가능합니다. 일반적으로 for_each를 권장합니다.
Q7. State Locking이 필요한 이유는?
두 명이 동시에 terraform apply를 실행하면 State가 충돌하여 인프라가 손상될 수 있습니다. DynamoDB 잠금 테이블로 동시 실행을 방지합니다.
Q8. 모듈(Module)의 장점과 설계 원칙은?
재사용성, DRY 원칙, 캡슐화가 장점입니다. 단일 책임 원칙으로 설계하고, 변수로 설정을 외부화하며, 출력값으로 다른 모듈과 연결합니다.
Q9. terraform import와 Import Block의 차이는?
terraform import는 명령줄에서 한 번에 하나의 리소스만 가져오며, 코드를 자동 생성하지 않습니다. Import Block(1.5+)은 선언적으로 여러 리소스를 한 번에 가져올 수 있고, -generate-config-out 옵션으로 코드 자동 생성이 가능합니다.
Q10. Terraform에서 시크릿을 안전하게 관리하는 방법은?
State 파일에 민감 정보가 저장되므로 원격 백엔드 암호화 필수, sensitive = true 변수 사용, HashiCorp Vault나 AWS Secrets Manager 연동, .tfvars 파일은 .gitignore에 추가, 환경변수로 주입합니다.
실전 (11-15)
Q11. Drift Detection이란 무엇이고 어떻게 처리하나요?
Drift는 실제 인프라가 Terraform 코드와 다른 상태입니다. terraform plan으로 감지하고, terraform apply로 코드 상태로 되돌리거나 코드를 수정합니다. 정기적인 plan 실행으로 드리프트를 모니터링해야 합니다.
Q12. Terragrunt를 사용하는 이유는?
Terraform의 DRY 원칙 부족을 보완합니다. 환경별 설정을 상속 구조로 관리하고, 원격 State 설정을 자동화하며, 모듈 간 의존성을 관리합니다.
Q13. Terraform CI/CD 파이프라인의 모범 사례는?
PR에서 plan 실행 후 결과를 코멘트로 게시, main 브랜치 merge 시 apply 실행, OIDC로 AWS 인증(시크릿 키 불필요), terraform fmt + tfsec으로 코드 품질 검증, 환경별 승인 프로세스(GitHub Environments) 적용합니다.
Q14. Terraform과 CloudFormation을 함께 사용해야 하는 경우는?
AWS 전용이고 CloudFormation StackSets, Service Catalog 등 AWS 네이티브 기능이 필요한 경우 CFN을, 멀티 클라우드나 SaaS 리소스 관리가 필요한 경우 Terraform을 사용합니다.
Q15. Infracost로 비용 관리하는 방법은?
PR에서 인프라 변경의 비용 영향을 자동으로 추정하여 코멘트로 보여줍니다. 월간 비용 증가/감소를 시각화하고, 팀이 비용을 인지한 상태에서 인프라 변경을 승인합니다.
15. 퀴즈
Q1. Terraform State의 원격 백엔드로 S3를 사용할 때 State Locking에 필요한 AWS 서비스는?
DynamoDB입니다. S3는 State 파일 저장, DynamoDB는 잠금(Lock) 테이블로 동시 실행을 방지합니다. dynamodb_table 속성으로 설정합니다.
Q2. for_each가 count보다 권장되는 이유는?
count는 인덱스 기반이라 중간 요소를 제거하면 그 뒤의 모든 리소스가 재생성됩니다. for_each는 키 기반이라 특정 요소만 안전하게 추가/삭제할 수 있습니다.
Q3. terraform plan -generate-config-out 옵션의 용도는?
Import Block으로 선언한 리소스의 HCL 코드를 자동 생성합니다. 기존 인프라를 Terraform으로 마이그레이션할 때 코드를 처음부터 작성할 필요 없이 자동 생성된 코드를 기반으로 정리하면 됩니다.
Q4. Terragrunt의 dependency 블록의 역할은?
모듈 간의 실행 순서와 데이터 전달을 관리합니다. 예를 들어 RDS 모듈이 VPC 모듈의 output(subnet_ids)에 의존하면, Terragrunt가 VPC를 먼저 적용하고 그 output을 RDS 모듈에 전달합니다.
Q5. tfsec와 Checkov의 차이점은?
tfsec는 Terraform 전용 보안 스캐너로 HCL 코드의 보안 취약점을 검사합니다. Checkov는 Terraform뿐 아니라 CloudFormation, Kubernetes, Docker 등 다양한 IaC 도구를 지원하는 범용 정책 검사 도구입니다.
참고 자료
- Terraform 공식 문서
- Terraform Registry
- Terraform AWS Provider
- Terraform GCP Provider
- Terragrunt 공식 문서
- Infracost 문서
- tfsec 가이드
- Checkov 문서
- HashiCorp Learn Terraform
- Terraform Up and Running (O'Reilly)
- AWS VPC Terraform Module
- Terraform Best Practices
- Atlantis 문서
- OpenTofu
- HashiCorp Vault Terraform Provider
- Terraform Associate 시험 가이드
- AWS IAM Terraform 모범 사례
- GKE Terraform 모듈