middlemoon
AWS Terraform으로 EC2 + ALB(HTTP→HTTPS 리디렉션) 전체 IaC 구축 후기 본문
최근 회사 클라우드 인프라를 Terraform 기반으로 관리할 준비를 하면서,
AWS 리소스(VPC, EC2, ALB, 보안그룹)를 모듈 단위로 IaC화 하는 작업을 했다.
처음에는 단순히 EC2 하나 만들면 되겠거니 했는데, 막상 Terraform으로 전체를 구성해보니
실제 AWS 콘솔에서 클릭으로 하던 모든 작업을 코드로 관리할 수 있다는 점이 굉장히 강력했다.
이번 과정에서 구축한 리소스는 다음과 같다.
Internet
↓ (80/443)
[ ALB ] — HTTPS Listener(443, ACM 인증서 적용)
│
├─ 80 → 443 Redirect Listener
│
↓ (8080)
[ Target Group ] → [ EC2 Instance ]
- 외부 요청은 ALB로 유입
- 80 포트는 자동으로 443으로 리다이렉션
- 443 리스너는 ACM 인증서 기반 HTTPS 처리
- ALB → EC2는 내부 포트 8080으로 트래픽 전달
- EC2 보안그룹은 ALB SG만 8080 접근 허용

terraform-iac/
├── main.tf
├── variables.tf
├── outputs.tf
│
├── modules/
│ ├── sg/
│ ├── ec2/
│ └── lb/
│
└── environments/
├── dev.tfvars
└── prod.tfvars
main.tf
→ 각 모듈을 호출
modules 폴더
→ 리소스별 (EC2, SG, LB) 코드 분리
environments/dev.tfvars
→ 실제 배포 환경값(region, vpc_id, subnet, key 등) 입력
terraform{
required_providers{
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
required_version = ">= 1.3.0"
}
provider "aws" {
region = var.region
}
#module "vpc" {
# source = "./modules/vpc"
# vpc_cidr = var.vpc_cidr
#}
module "sg" {
source = "./modules/sg"
vpc_id = var.vpc_id
}
module "ec2" {
source = "./modules/ec2"
subnet_id = "subnet-08f49a26209c39ba4"
sg_id = module.sg.ec2_sg_id
key_name = var.key_name
instance_type = var.instance_type
}
module "lb" {
source = "./modules/lb"
vpc_id = var.vpc_id
subnet_ids = var.subnet_ids
sg_id = module.sg.alb_sg_id
instance_id = module.ec2.instance_id
certificate_arn = var.certificate_arn
}
AWS 인증 기반 EC2에서 Terraform 실행
Terraform을 로컬 Windows에서 실행하는 것이 아니라,
AWS EC2에 IAM Role을 붙여서 바로 Terraform을 실행했다.
IAM Role에 다음 권한만 부여:
✔ AmazonEC2FullAccess
EC2 터미널에서 IAM Role이 정상 동작하는지 확인:


보안그룹(SG) 모듈 설계 – 가장 많이 실수하는 구간
이번 구축에서 가장 중요한 부분은 EC2와 ALB의 보안그룹 관계 설정이었다.
resource "aws_security_group" "alb_sg" {
name = "terraform-alb-sg"
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "terraform-alb-sg"
}
}
resource "aws_security_group" "ec2_sg" {
name = "terraform-web-sg-v2" #보안 그룹 이름
vpc_id = var.vpc_id
ingress {
description = "Allow inbound only from my IP"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["121.166.55.61/32"]
}
# ingress {
# description = "Allow 8080 from ALB"
# from_port = 8080
# to_port = 8080
# protocol = "tcp"
# source_security_group_id = aws_security_group.alb_sg.id #cidr_blocks를 대신해서 사용.
#}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "terraform-ec2-sg-v2" # Name
}
}
# ⬇⬇⬇ 여기가 핵심: ALB → EC2 8080 포트 규칙 ⬇⬇⬇
resource "aws_security_group_rule" "allow_alb_to_ec2_8080" {
type = "ingress"
from_port = 8080
to_port = 8080
protocol = "tcp"
# 8080을 열어줄 대상(EC2 보안 그룹)
security_group_id = aws_security_group.ec2_sg.id
# ALB에서만 접근 허용
source_security_group_id = aws_security_group.alb_sg.id
}
처음에는 cidr_blocks = […] 에 SG ID를 넣어서 오류가 났다.
SG는 CIDR이 아니므로 security_groups 로 넣어야 한다.
이거 몰라서 한참 삽질했다.....
EC2는 Terraform으로 만들지만, 서비스는 직접 실행해야 한다
Terraform으로 만든 EC2는 빈 Amazon Linux 이다.
즉, ALB가 8080으로 health check를 보내도 EC2가 응답하지 않는 상태였다.
그래서 health check가 계속 unhealthy → ALB 리스너가 “연결할 수 없음” 표시됨.
해결: EC2에서 간단한 웹서버 실행

python3 -m http.server 8080 --bind 0.0.0.0


ALB 구축 – HTTP → HTTPS 리디렉션 + HTTPS Listener
LB 모듈의 핵심 코드:
resource "aws_lb" "this" {
name = "terraform-alb"
load_balancer_type = "application"
security_groups = [var.sg_id]
subnets = var.subnet_ids
tags = {
Name = "dev-terraform-alb"
}
}
resource "aws_lb_target_group" "this" {
name = "terraform-tg"
port = 8080
protocol = "HTTP"
target_type = "instance"
vpc_id = var.vpc_id
health_check {
path = "/"
protocol = "HTTP"
matcher = "200,301,302"
interval = 30
}
tags = {
Name = "dev-terraform-tg"
}
}
resource "aws_lb_target_group_attachment" "attach" {
target_group_arn = aws_lb_target_group.this.arn
target_id = var.instance_id
port = 8080
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.this.arn
port = "80"
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.this.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
certificate_arn = var.certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.this.arn
}
}

이 명령은 현재 실행한 디렉토리의 파일들을 그대로 웹에서 노출시키는 단순 서버라서
~(ec2-user 홈 디렉토리)에 있는 .ssh, .bashrc 등이 그대로 보이는 것임.
apply 를 반복해도 Terraform은 재생성하지 않는다
처음에는 apply를 여러 번 누르면 EC2나 ALB가 계속 추가로 생성되는지 걱정했는데,
Terraform은 "상태파일(tfstate)" 를 기반으로 이미 만든 리소스를 추적한다.
즉,
✔ 코드 변경 없음 → 아무 일도 안함
✔ 코드 일부 수정 → 해당 리소스만 수정
✔ 삭제 → destroy 명령 필요
AWS 콘솔처럼 클릭으로 리소스를 만들어놓는 게 아니라,
코드가 곧 인프라의 진실(source of truth) 이다.
이번 IaC 구축을 통해 배운 것 (핵심)
이번 과정을 통해 실무형 IaC를 구축하려면 아래 5가지를 반드시 이해해야 한다.
1) Terraform의 디렉토리 구조 설계
modules / environments / main.tf 구조화
2) 보안그룹의 상호 참조 방식
SG → SG 허용은 cidr_blocks가 아니라 security_groups
3) ALB → EC2 health check 방식
EC2에 서비스가 없으면 ALB는 무조건 비정상
4) HTTP → HTTPS redirect 구현
redirect listener + https listener 구성
5) Terraform은 apply마다 새 리소스를 만들지 않는다
상태 기반 관리 → 변경분만 반영
결론: 클라우드 엔지니어링의 핵심은 “반복 가능한 인프라”
처음에는 무작정 EC2 하나 Terraform으로 만들겠다고 시작했지만,
ALB → EC2 → SG → 인증서까지 전체 인프라 구성을 코드로 묶어보니
진짜 DevOps / Cloud Engineer가 일하는 방식이 보였다.
특히,
- AWS 콘솔에서 클릭으로 만들던 구조를
- 완전히 코드로 재현할 수 있고
- 언제든 다른 환경(dev/prod)에도 재생성 가능하고
- 인프라 변경 히스토리가 모두 Git에 남고
- 재현성 있는 인프라(immutable Infrastructure)를 만들 수 있다는 점이 큰 장점.