개발팀이 늘고 환경이 많아지면 AWS 콘솔에서 클릭으로 인프라를 만드는 방식의 문제가 드러난다. “어떻게 만들었는지” 기록이 없고, 재현이 어렵고, 누가 무엇을 바꿨는지 추적이 안 된다. 스테이징과 프로덕션 환경이 언제부터 달라졌는지 모른다.
IaC는 인프라 구성을 코드로 작성해 버전 관리 시스템(git)에 저장하는 방식이다. 코드 리뷰, CI/CD, 자동화된 배포가 가능해진다.
선언형 vs 명령형
명령형 (Imperative)
“어떻게 만들지"를 순서대로 기술한다. AWS CLI 스크립트가 대표적이다.
aws ec2 create-vpc --cidr-block 10.0.0.0/16
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=cidr,Values=10.0.0.0/16" --query 'Vpcs[0].VpcId' --output text)
aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.1.0/24
aws ec2 create-internet-gateway
IGW_ID=$(...)
aws ec2 attach-internet-gateway --vpc-id $VPC_ID --internet-gateway-id $IGW_ID
현재 상태를 모른다. 이미 VPC가 있을 때 다시 실행하면 중복 생성하거나 오류가 난다. 멱등성이 없다.
선언형 (Declarative)
“어떤 상태이길 원하는지"를 기술한다. 도구가 현재 상태와 비교해 필요한 변경만 적용한다.
# Terraform
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
이미 VPC가 있으면 아무것도 하지 않는다. CIDR을 바꾸면 변경 사항만 적용한다. 현재 상태와 desired state 사이의 diff를 계산해 적용한다 — k8s의 선언형 모델과 같은 철학이다.
Terraform
HashiCorp가 만든 가장 널리 쓰이는 IaC 도구다. HCL(HashiCorp Configuration Language)로 인프라를 선언한다. AWS, GCP, Azure, k8s 등 수백 개의 프로바이더를 지원한다.
# main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "my-terraform-state"
key = "production/terraform.tfstate"
region = "ap-northeast-2"
}
}
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
tags = {
Name = "${var.env}-vpc"
Environment = var.env
}
}
variable "vpc_cidr" {
default = "10.0.0.0/16"
}
variable "env" {
default = "production"
}
terraform init # 프로바이더 다운로드
terraform plan # 변경 사항 미리 보기
terraform apply # 적용
terraform destroy # 모든 리소스 삭제
terraform plan은 “이렇게 변경됩니다"를 보여준다. 팀원이 이 결과를 리뷰하고 merge하면 CI/CD가 apply를 실행하는 흐름이 표준이다.
State 파일 문제
Terraform은 현재 상태를 state 파일에 저장한다. 현재 상태와 코드를 비교해 diff를 계산한다.
terraform.tfstate ← 현재 인프라 상태가 JSON으로 저장됨
이 파일에 몇 가지 문제가 있다.
팀 협업: 로컬에 state 파일이 있으면 팀원들이 공유할 수 없다. S3 같은 원격 backend에 저장하고 DynamoDB로 잠금(locking)을 걸어야 동시에 두 명이 apply하는 사고를 막을 수 있다.
민감 정보: state 파일에 DB 비밀번호 같은 민감 정보가 평문으로 저장될 수 있다. S3 암호화와 접근 제어가 필요하다.
드리프트: 콘솔에서 직접 인프라를 수정하면 state 파일과 실제 상태가 달라진다. terraform refresh로 동기화하거나 terraform import로 수동으로 맞춰야 한다. IaC를 도입하면 콘솔 직접 수정을 금지하는 문화가 함께 필요하다.
Pulumi
Terraform과 같은 목적이지만 다른 철학을 가진다. HCL 대신 일반 프로그래밍 언어(TypeScript, Python, Go, Java)로 인프라를 작성한다.
// index.ts
import * as aws from "@pulumi/aws";
const vpc = new aws.ec2.Vpc("main", {
cidrBlock: "10.0.0.0/16",
enableDnsHostnames: true,
});
const subnets = ["10.0.1.0/24", "10.0.2.0/24"].map((cidr, i) =>
new aws.ec2.Subnet(`public-${i}`, {
vpcId: vpc.id,
cidrBlock: cidr,
})
);
export const vpcId = vpc.id;
루프, 조건문, 함수, 타입 시스템을 그대로 쓸 수 있다. 여러 환경을 만들거나 복잡한 조건 분기가 있을 때 HCL보다 훨씬 표현력이 좋다. state 관리는 Pulumi Cloud 또는 S3 backend를 쓴다.
비교
| Terraform | Pulumi | |
|---|---|---|
| 언어 | HCL | TypeScript, Python, Go 등 |
| 학습 곡선 | 낮음 (선언적 DSL) | 기존 언어 알면 낮음 |
| 표현력 | 제한적 | 높음 (일반 언어) |
| 생태계 | 매우 넓음 (프로바이더 수) | 넓음 |
| 상태 관리 | self-managed or Terraform Cloud | Pulumi Cloud or self-managed |
트레이드오프
IaC를 도입하면 인프라 변경이 코드 리뷰 → PR → CI/CD 흐름을 타야 한다. 긴급 상황에서 빠르게 수동으로 콘솔을 건드리고 싶은 상황과 충돌한다. 조직의 성숙도와 긴급 처리 절차를 함께 설계해야 한다.
모듈화를 과하게 하면 코드가 오히려 파악하기 어려워진다. 세 군데에 쓰이기 전에 모듈을 만들지 않는 원칙(Rule of Three)이 IaC에도 유효하다.