Terraform으로 최소 비용 실습 인프라 만들기
⚠️ 사전 안내
- 이 구성은 실습용이다. 프로덕션에서는 단일 NAT Instance 같은 SPOF 구조를 권장하지 않는다.
- 운영 환경에서는 GSLB + 다중 센서(다중 AZ/다중 인스턴스) 구조를 권장한다.
- Terraform이 익숙하지 않으면: AWS Console에서 리소스를 수동으로 만들고이 편(1편)은 스킵하고
다음 편의 Suricata/ELK 설치 및 시그니처 테스트 가이드부터 바로
참조하도록 한다.- 인스턴스 사양 관련 참고 사항
본 실습에서는 Suricata(NAT Instance)와 타겟 서버를 모두t3.medium으로 구성했다.
이는 초기 구성과 성능 검증을 단순화하기 위한 선택이다.
다만 비용을 고려하면 두 인스턴스를 동일 사양으로 유지할 필요는 없다.
– Suricata NAT Instance
패킷 처리, NFQUEUE, ELK(Logstash/Elasticsearch)까지 함께
수행하므로 최소t3.medium이상을 권고한다.
– 웹 서버 / PBX 타겟 인스턴스
단순 트래픽 발생 및 공격 테스트 용도이므로t3.micro또는t3.small로 낮춰도 무관하다.
단, 이 경우 타겟 인스턴스의 사양 변경은 Terraform 코드에서
직접 수정해야 한다.
(본 실습에서는 단순화를 위해 동일 사양으로 유지한다)
1. 목표
- NAT Instance 위에 Suricata + ELK를 구성 (비용문제로 opensearch 사용안함)
- Target 서버(Private Subnet)를 만들고 모든 트래픽이 NAT를 강제 경유하도록 라우팅 구성
- EIP를 붙여서 NAT의 퍼블릭 IP를 고정
- Terraform으로 한 번에 생성 / 한 번에 삭제
📌 Terraform 코드 적용 범위에 대한 안내 (중요)
이 시리즈에서 제공하는 Terraform 코드는
Suricata Inline IPS 구성을 설명하기 위한 핵심 예제만을 포함한다.
실제 운영 환경에서는 이미 다음과 같은 리소스들이
각자 다른 구조와 코드로 관리되고 있을 가능성이 높다.
- VPC
- Subnet (Public / Private)
- Route Table
- Internet Gateway
- 기존 EC2 및 Security Group
- 기타 네트워크 모듈
고로, 각자의 운영환경이 달라서 오히려 혼선만 초래할 우려가 있어, 이 시리즈에서는
내가 운영하고 있는 환경의 전체 Terraform 코드를 모두 공개하지 않는다.
대신 다음 원칙을 기준으로 설명한다.
이 시리즈의 Terraform 가이드 원칙
- Suricata와 직접적으로 연관된 리소스만 코드로 제시
- 기존 인프라는
data리소스로 참조하거나- 상위 모듈에서 변수로 전달받는 것을 전제로 설명
- 독자는 자신의 Terraform 구조에 맞게 응용해야 한다
즉, 이 시리즈의 코드는:
복사·붙여넣기용이 아니라
구조 이해와 응용을 위한 기준 예제다.
2. 아키텍처

핵심은 2개다.
- NAT Instance는 source/dest check OFF
- Private Subnet 라우팅은 0.0.0.0/0 → NAT Instance ENI 로 강제
3. 폴더 구조
이미지처럼 모듈은 파일 3개로 구분한다.
my-site/
├─ envs/
│ └─ prod/
│ ├─ main.tf
│ ├─ variables.tf
│ └─ terraform.tfvars
└─ modules/
└─ suricata_lab/
├─ main.tf
├─ variables.tf
└─ outputs.tf
modules/suricata_lab/main.tf에 리소스 전부 몰아 넣는다.variables.tf,outputs.tf에는 정의만 최소로 둔다.envs/prod는 모듈 호출 + 변수 선언만 한다.
4. envs/prod 쪽에 추가해야 하는 것
4.1 envs/prod/variables.tf
variable "key_name" {
type = string
description = "EC2 key pair name"
}
4.2 envs/prod/main.tf
(기존 다른 모듈이 이미 있어도 상관 없음. 아래 모듈만 추가하면 됨)
module "suricata_lab" {
source = "../../modules/suricata_lab"
name = "sec-lab"
key_name = var.key_name
my_ip = "162.120.XXX.XXX/32" #실제 접속할 자신의 IP 적용
instance_type = "t3.medium"
}
5. modules/suricata_lab 코드
✅ modules/suricata_lab/main.tf
############################################################
# modules/suricata_lab/main.tf
# - Ubuntu 24.04
# - NAT Instance (Suricata IPS + ELK) in Public Subnet
# - Target EC2 in Private Subnet
# - Private RT: 0.0.0.0/0 -> NAT Instance
# - NAT Instance: source_dest_check = false (필수)
############################################################
############################
# VPC / Subnet / IGW
############################
resource "aws_vpc" "lab" {
cidr_block = "10.20.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.name}-vpc"
}
}
resource "aws_internet_gateway" "lab" {
vpc_id = aws_vpc.lab.id
tags = {
Name = "${var.name}-igw"
}
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.lab.id
cidr_block = "10.20.0.0/24"
map_public_ip_on_launch = true
tags = {
Name = "${var.name}-public"
}
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.lab.id
cidr_block = "10.20.1.0/24"
tags = {
Name = "${var.name}-private"
}
}
############################
# Route Table - Public
############################
resource "aws_route_table" "public" {
vpc_id = aws_vpc.lab.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.lab.id
}
tags = {
Name = "${var.name}-rt-public"
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
############################
# Security Group - NAT/Suricata/ELK
############################
resource "aws_security_group" "nat" {
name = "${var.name}-sg-nat"
description = "NAT Instance (Suricata+ELK) SG"
vpc_id = aws_vpc.lab.id
# SSH (관리)
ingress {
description = "SSH from my IP"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.my_ip]
}
# HTTP 접속허용 (only my ip)
ingress {
description = "HTTP from my IP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [var.my_ip]
}
# Kibana
ingress {
description = "Kibana from my IP"
from_port = 5601
to_port = 5601
protocol = "tcp"
cidr_blocks = [var.my_ip]
}
#private target 허용
ingress {
description = "All from target ip"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["10.20.1.0/24"]
}
# Outbound all
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.name}-sg-nat"
}
}
############################
# Security Group - Target (Private)
############################
resource "aws_security_group" "target" {
name = "${var.name}-sg-target"
description = "Target EC2 (private) SG"
vpc_id = aws_vpc.lab.id
# SSH (관리)
ingress {
description = "SSH from NAT Instance only"
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.nat.id]
}
# HTTP: NAT SG만 허용(필요하면)
ingress {
description = "HTTP 80 from NAT SG"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.nat.id]
}
# Outbound all
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.name}-sg-target"
}
}
############################
# EC2 - NAT/Suricata/ELK (Public)
############################
resource "aws_instance" "nat" {
ami = "ami-0a71e3eb8b23101ed"
instance_type = var.instance_type
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.nat.id]
key_name = var.key_name
associate_public_ip_address = true
root_block_device {
volume_size = 20
volume_type = "gp3"
delete_on_termination = true
}
# NAT Instance 필수
source_dest_check = false
tags = {
Name = "${var.name}-nat-suricata-elk"
}
}
############################
# EC2 - Target (Private)
############################
resource "aws_instance" "target" {
ami = "ami-0a71e3eb8b23101ed"
instance_type = var.instance_type
subnet_id = aws_subnet.private.id
vpc_security_group_ids = [aws_security_group.target.id]
key_name = var.key_name
tags = {
Name = "${var.name}-target"
}
}
############################
# Route Table - Private
# - 0.0.0.0/0 -> NAT Instance
############################
resource "aws_route_table" "private" {
vpc_id = aws_vpc.lab.id
route {
cidr_block = "0.0.0.0/0"
network_interface_id = aws_instance.nat.primary_network_interface_id
}
tags = {
Name = "${var.name}-rt-private"
}
}
resource "aws_route_table_association" "private" {
subnet_id = aws_subnet.private.id
route_table_id = aws_route_table.private.id
}
# NAT Instance용 EIP
resource "aws_eip" "nat" {
domain = "vpc"
tags = {
Name = "${var.name}-eip-nat"
}
}
# NAT Instance에 EIP 연결
resource "aws_eip_association" "nat" {
allocation_id = aws_eip.nat.id
instance_id = aws_instance.nat.id
}
6. modules/suricata_lab/variables.tf
리소스는 main.tf에 전부 들어갔고, 여기는 변수 선언만 둔다.
✅ modules/suricata_lab/variables.tf
variable "name" {
type = string
description = "Lab name prefix"
}
variable "region" {
type = string
default = "ap-northeast-2"
}
variable "key_name" {
type = string
}
variable "my_ip" {
type = string
description = "162.120.xxx.xxx/32" # 실제 접속할 IP 적용
}
variable "instance_type" {
type = string
default = "t3.medium"
}
7. modules/suricata_lab/outputs.tf
outputs는 최소로만 둔다.
실습할 때 NAT 접속/타겟 확인 용도만 출력.
✅ modules/suricata_lab/outputs.tf
output "nat_public_ip" {
value = aws_eip.nat.public_ip
}
output "target_private_ip" {
value = aws_instance.target.private_ip
}
8. terrafrom 실행할 때 변수 주입 (윈도우 환경)
키페어는 Terraform에서 만들 필요 없다.AWS console에서 만든 걸 이름만 넣는다.
##visual studio code 터미널에서 아래경로 이동##
cd C:\projects\my-site\envs\prod
PS C:\projects\my-site\envs\prod> terraform init
##key pair 확인##
PS C:\projects\my-site\envs\prod> aws ec2 describe-key-pairs --region ap-northeast-2 --query "KeyPairs[].KeyName" --output table
##key pair 변수 주입 및 테라폼 실행##
PS C:\projects\my-site\envs\prod>terraform apply -var="key_name=blog"
Step-by-Step: 실제 테라폼 실행과 인프라 생성 과정





9. 이 편(1편)에서 끝나는 지점
여기까지 하면:
- NAT Instance (Suricata + ELK 자리) 생성
- EIP 연결 완료 (고정 Public IP)
- Private Subnet 트래픽 NAT 강제 경유 라우팅 완료
- Target 서버 생성 완료
이제 다음 편에서 할 일은 명확하다.
- NAT 인스턴스에 Suricata + ELK 설치
- ip_forward + iptables(NFQUEUE) 구성
- 룰 업데이트 + SIP 시그니처 검증
마무리 및 당부 : Terraform이 익숙하지 않다면
- Console에서 VPC/Subnet/IGW/RT/EC2/EIP를 수동 생성하고
- 이 편은 스킵하고
- 2편부터 따라가면 된다.
🛠 마지막 수정일: 2025.12.19
ⓒ 2025 엉뚱한 녀석의 블로그 [quirky guy's Blog]. All rights reserved. Unauthorized copying or redistribution of the text and images is prohibited. When sharing, please include the original source link.
💡 도움이 필요하신가요?
Zabbix, Kubernetes, 그리고 다양한 오픈소스 인프라 환경에 대한 구축, 운영, 최적화, 장애 분석,
광고 및 협업 제안이 필요하다면 언제든 편하게 연락 주세요.
📧 Contact: jikimy75@gmail.com
💼 Service: 구축 대행 | 성능 튜닝 | 장애 분석 컨설팅
📖 E-BooK [PDF] 전자책 (Gumroad):
Zabbix 엔터프라이즈 최적화 핸드북
블로그에서 다룬 Zabbix 관련 글들을 기반으로 실무 중심의 지침서로 재구성했습니다.
운영 환경에서 바로 적용할 수 있는 최적화·트러블슈팅 노하우까지 모두 포함되어 있습니다.
💡 Need Professional Support?
If you need deployment, optimization, or troubleshooting support for Zabbix, Kubernetes,
or any other open-source infrastructure in your production environment, or if you are interested in
sponsorships, ads, or technical collaboration, feel free to contact me anytime.
📧 Email: jikimy75@gmail.com
💼 Services: Deployment Support | Performance Tuning | Incident Analysis Consulting
📖 PDF eBook (Gumroad):
Zabbix Enterprise Optimization Handbook
A single, production-ready PDF that compiles my in-depth Zabbix and Kubernetes monitoring guides.
답글 남기기
댓글을 달기 위해서는 로그인해야합니다.