| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- Amazon S3
- 드림핵
- terraform
- AWS 인프라 아키텍처
- operating system
- AWS
- AWS 침해 사고 사례 분석
- TryHackMe
- AWS 보안 아키텍처 분석
- C
- python
- AWS 아키텍처 분석
- 프로그래머스
- reversing.kr
- 네트워크
- 침입 차단 시스템(IPS)
- AWS 3 Tier Architecture
- 리버싱
- AWS IAM Role
- AWS 인프라 분석
- AWS 사고 사례 분석
- dreamhack
- 운영체제
- reversing
- AWS 보안 사고 사례 모음
- network
- programmers
- AWS 침해사고 사례 분석
- AWS Active Directory
- IAM Federation
- Today
- Total
lhywk 님의 블로그
AWS 3-Tier 및 Data Pipeline 구축 With Terraform (3) 본문
AWS 3 Tier Architecture 구축해보기 2-2
https://github.com/estrellaSia/AWS_3Tier_Infra_Data_Pipeline/tree/main참고 깃허브 Main.tf 작성하기(2) 1. ALB, ALB SG# ALBresource "aws_lb" "alb-web" {name = "ddong-alb-web"internal = falseload_balancer_type = "application" # Application Load Balan
kujung.tistory.com

1. ALB, ALB SG
# ALB
resource "aws_lb" "alb-web" {
name = "ho-alb-web"
internal = false
load_balancer_type = "application" # Application Load Balancer
security_groups = [aws_security_group.alb-sg-web.id]
subnets = [aws_subnet.pub-sub1.id, aws_subnet.pub-sub2.id]
}
resource "aws_lb" "alb-was" {
name = "ho-alb-was"
internal = true
load_balancer_type = "application"
security_groups = [aws_security_group.alb-sg-was.id]
subnets = [aws_subnet.was-sub1.id, aws_subnet.was-sub2.id]
}
# ALB SG
resource "aws_security_group" "alb-sg-web" { # Web ALB SG
name = "ho-alb-sg-web"
description = "ALB Security Group"
vpc_id = aws_vpc.vpc.id
ingress {
description = "HTTP from Web Tier"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS from web Tier"
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 = "ho-alb-sg-web"
}
}
resource "aws_security_group" "alb-sg-was" { # Was ALB SG
name = "ho-alb-sg-was"
description = "ALB Security Group"
vpc_id = aws_vpc.vpc.id
ingress {
description = "HTTP from Internet"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.asg-sg-web.id]
# asg-security-group-web이라는 SG에 속한 인스턴스만이 이 포트를 통해 ALB에 접근할수 있도록 제한
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "ho-alb-sg-was"
}
}
resource "aws_lb" "alb-web/was"
이 리소스는 트래픽을 각 서버 계층으로 분산해주는 역할
- resource "aws_lb" "alb-web"
- internal = false: 외부 인터넷에서 접근 가능한 Public ALB로 설정. 사용자의 첫 진입점
- subnets: 로드밸런서가 위치할 서브넷. Public ALB이므로 이전에 만든 Public Subnet 2개를 지정
- resource "aws_lb" "alb-was"
- internal = true: VPC 내부에서만 접근 가능한 Internal ALB. Web 계층에서 전달된 요청을 WAS 계층으로 넘겨주는 역할
- subnets: WAS 전용 프라이빗 서브넷들을 지정하여 계층 간 격리를 구현
resource "aws_security_group" "alb-sg-web"
Public ALB를 위한 SG
- ingress (HTTP/HTTPS): 0.0.0.0/0으로부터 80(HTTP)과 443(HTTPS) 포트 접속을 허용. 웹 서비스의 일반적인 설정
- egress: 모든 포트에 대해 외부로 나가는 통신을 허용
resource "aws_security_group" "alb-sg-was"
- security_groups = [aws_security_group.asg-sg-web.id]:
- 특정 보안 그룹(asg-sg-web)을 가진 리소스의 접근만 허용
- 웹 서버의 IP가 오토스케일링으로 인해 수시로 변하더라도 해당 보안 그룹만 가지고 있다면 통신이 허용
2. SSM 접속을 위한 IAM
resource "aws_iam_role" "ec2_ssm_role" {
name = "ho-EC2SSM"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}]
})
}
# IAM 역할 정책 연결
resource "aws_iam_role_policy_attachment" "ec2_ssm_policy_attachment" {
for_each = toset([
"arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess",
"arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
"arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole"
])
role = aws_iam_role.ec2_ssm_role.name
policy_arn = each.key
}
# EC2 인스턴스 프로파일 생성
# EC2 인스턴스를 구분하고 그 인스턴스에 권한을 주기 위한 개념
# 인스턴스 프로파일이 지정된 EC2는 시작 시 역할 정보를 받아오고 해당 역할로 필요한 권한들을 얻게 됨
resource "aws_iam_instance_profile" "ec2_ssm_instance_profile" {
name = "ho-EC2SSM-Instance-Profile"
role = aws_iam_role.ec2_ssm_role.name
}
resource "aws_iam_role" "ec2_ssm_role" 분석
이 부분은 "누가 이 역할을 수행할 수 있는가?"를 정의하는 단계
- resource "aws_iam_role" "ec2_ssm_role"
- aws_iam_role: AWS에서 특정 권한의 묶음(Job Description)인 IAM 역할을 생성
- assume_role_policy:
- jsonencode 내부를 보면 Service = "ec2.amazonaws.com"이라고 되어 있다. 이는 이 역할은 오직 EC2 서비스만 가져다가 쓸 수 있다라고 선언
resource "aws_iam_role_policy_attachment" "ec2_ssm_policy_attachment"
이 부분은 "이 신분증을 가지면 어떤 일을 할 수 있는가?"를 정의하는 단계
- for_each = toset([...]):
- 테라폼의 반복문. 아래 3가지 정책을 하나씩 돌아가며 이 역할에 붙여줌.
- 연결된 정책들:
- AmazonS3ReadOnlyAccess: S3 버킷에 저장된 파일을 읽을 수 있는 권한
- AmazonSSMManagedInstanceCore: 이 권한이 있어야만 Session Manager를 통해 SSH 키 없이도 브라우저에서 서버에 접속하거나 관리할 수 있다
- AWSCodeDeployRole: 아키텍처 다이어그램 왼쪽에 있던 CodeDeploy가 이 서버에 코드를 배포할 수 있도록 허용하는 권한
resource "aws_iam_instance_profile" "ec2_ssm_intance_profile"
이 부분은 "어떻게 이 신분증을 EC2에게 전달할 것인가?"를 결정하는 단계
- resource "aws_iam_instance_profile" "ec2_ssm_intance_profile"
- 인스턴스 프로파일: AWS 콘솔에서는 역할을 만들면 자동으로 만들어지기도 하지만 테라폼에서는 명시적으로 만들어줘야 함
- 역할: EC2 인스턴스는 '역할'을 직접 입을 수 없고, '인스턴스 프로파일'이라는 주머니에 담긴 '역할'을 전달받는 구조. 나중에 EC2 코드를 짤 때 이 프로파일 이름을 적어주게 된다.
3. Web 시작 템플릿
resource "aws_launch_template" "template-web" {
name = "ho-launch-template-web"
image_id = "ami-0ecfdfd1c8ae01aec" # Amazon Linux 2023 등 최신 AMI 확인 필요
instance_type = "t3.micro"
# IMDSv2 설정: 인스턴스 메타데이터 탈취 공격 방어
metadata_options {
http_endpoint = "enabled"
http_tokens = "required" # 토큰 없는 요청 거부 (보안 강화)
http_put_response_hop_limit = 1 # 외부에서의 비정상 접근 방지
instance_metadata_tags = "enabled"
}
# 네트워크 및 보안 그룹 설정
network_interfaces {
device_index = 0
security_groups = [aws_security_group.asg-sg-web.id]
}
# 이전 단계에서 만든 '신분증 케이스' 전달
iam_instance_profile {
name = aws_iam_instance_profile.ec2_ssm_instance_profile.name
}
# 실행 시 WAS ALB의 주소를 주입 (Web -> WAS 통신용)
user_data = base64encode(templatefile("web-user-data.sh", {
alb_dns = "${aws_lb.alb-was.dns_name}"
}))
depends_on = [aws_lb.alb-web]
tag_specifications {
resource_type = "instance"
tags = { Name = "ho-web-instances" }
}
}
- resource "aws_launch_template" "template-web": 나중에 오토스케일링 그룹(ASG)이 인스턴스를 찍어낼 때 참조할 틀을 정의
- image_id & instance_type:최신 x86 AMI와 t3.micro를 적용 (별도로 확인 필요)
- http_tokens = "required": IMDSv2(Instance Metadata Service v2)를 강제SSRF(서버 측 요청 위조) 공격을 통해 공격자가 인스턴스의 IAM 역할 정보를 탈취하는 것을 원천 봉쇄. 토큰 기반의 세션 방식을 사용하여 보안 수준을 극대화한 설정
- http_put_response_hop_limit = 1: 응답이 네트워크 홉을 한 번 이상 넘지 못하게 하여 인스턴스 밖으로 메타데이터 응답이 유출되는 것을 방지
- network_interfaces: asg-sg-web 보안 그룹을 지정
- iam_instance_profile:이전에 만든 프로파일을 씌워줌. 이 덕분에 서버가 태어나자마자 SSM이나 S3에 접근할 수 있다.
- templatefile("web-user-data.sh", { ... }):미리 작성된 .sh 스크립트 파일에 테라폼의 변수를 실시간으로 꽂아 넣음
- alb_dns = "${aws_lb.alb-was.dns_name}":Web 서버는 사용자의 요청을 처리한 후 내부 로드밸런서(WAS ALB)로 트래픽을 넘겨야 한다. 그래서 WAS ALB의 주소를 인자로 전달하는 것
- depends_on = [aws_lb.alb-web]:로드밸런서가 먼저 존재해야 서버가 트래픽을 받을 준비를 할 수 있으므로, 생성 순서를 명확히 지정했습니다.
- tag_specifications:템플릿 자체가 아니라 이 템플릿으로 생성될 실제 EC2 인스턴스들에 ho-web-instances라는 이름을 붙여줌.
4. Was 시작 템플릿
# 2. WAS 시작 템플릿 선언문
resource "aws_launch_template" "template-was" {
name = "ho-launch-template-was"
image_id = "ami-0ecfdfd1c8ae01aec"
instance_type = "t3.micro"
network_interfaces {
device_index = 0
security_groups = [aws_security_group.asg-sg-was.id]
}
iam_instance_profile {
name = aws_iam_instance_profile.ec2_ssm_instance_profile.name
}
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 1
instance_metadata_tags = "enabled"
}
# DB 접속 정보를 스크립트에 주입
user_data = base64encode(templatefile("app-user-data.sh", {
host = "${local.host}"
rds_endpoint = "${aws_db_instance.rds-db.endpoint}" # data 소스 대신 직접 참조 권장
username = "ho_admin"
password = "ho_password123!" # 실제 운영시에는 Secret Manager 사용 권장
db = "hodb"
}))
depends_on = [aws_db_instance.rds-db]
tag_specifications {
resource_type = "instance"
tags = { Name = "ho-was-instances" }
}
}
- resource "aws_launch_template" "template-was": WAS 서버를 생성하기 위한 '원본 틀'
- security_groups = [aws_security_group.asg-sg-was.id]: WAS 전용 보안 그룹. 앞서 설계한 대로 Internal ALB에서 오는 트래픽만 골라서 받도록 설정.
- iam_instance_profile: Web 서버와 동일하게 SSM/S3/CodeDeploy 권한을 부여.
- metadata_options: Web 서버와 동일한 보안 설정을 적용. 특히 WAS 서버는 DB 접속 정보 등 민감한 데이터를 다룰 가능성이 높으므로, 토큰(http_tokens = "required")을 강제하여 메타데이터 탈취를 막는 것이 중요
- templatefile("app-user-data.sh", { ... }): WAS 서버는 데이터베이스에 접속해야 함. 이를 위해 DB 주소, 계정명, 비밀번호 등의 환경 변수를 런타임에 주입
- 변수 매핑:
- host = "${local.host}": locals에서 포트 번호를 떼어낸 순수 DB 주소를 전달
- rds_endpoint: 생성된 RDS의 전체 엔드포인트 주소를 가져옴
- username, password, db: 애플리케이션 코드가 DB에 로그인할 때 사용할 자격 증명
- depends_on = [aws_db_instance.rds-db]: DB가 아직 만들어지지도 않았는데 WAS 서버가 먼저 뜨면 접속 에러가 남. 테라폼에게 "DB가 완전히 생성된 후에 이 템플릿을 활성화해라"라고 명령하는 것
- tag_specifications: 생성될 서버들에게 ho-was-instances라는 이름표를 달아주어 Web 서버와 구분
# 3. 로컬 변수 선언문
locals {
# RDS 엔드포인트는 보통 '주소:포트' 형태인데, 여기서 포트(:3306)를 제거하여 순수 호스트 주소만 추출
# replace(대상, "찾을문자", "바꿀문자") 함수를 사용하여 가공
host = replace(aws_db_instance.rds-db.endpoint, ":3306", "")
}
locals { ... }
- 테라폼 코드 전체에서 공통으로 사용할 수 있는 변수를 선언
- 한 번 계산된 값을 여러 리소스(aws_launch_template 등)에서 local.host라는 이름으로 재사용할 수 있다
host = replace(aws_db_instance.rds-db.endpoint, ":3306", "")
- 원래 값 (aws_db_instance.rds-db.endpoint):
- AWS RDS가 생성되면 보통 terraform-db.xxxx.ap-northeast-2.rds.amazonaws.com:3306과 같은 형태의 주소를 줌
- 끝에 :3306(포트 번호)이 붙어 있다
- replace(대상, 찾을값, 바꿀값):
- 대상: RDS 엔드포인트 문자열
- 찾을값: :3306
- 바꿀값: "" (빈 문자열)
- 결과값 (host):
- 포트 번호가 삭제된 terraform-db.xxxx.ap-northeast-2.rds.amazonaws.com만 남게 됨
5. Target Group
# TG-Web
resource "aws_lb_target_group" "tg-web" {
name = "ho-tg-web"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.vpc.id
health_check {
path = "/"
matcher = "200-299"
# health check 위해 기대되는 http 응답 코드 범위(200~299: 성공 응답)
interval = 5 # 5초마다 health check 수행
timeout = 3 # 3초 내에 반환하지 않으면 실패로 간주
healthy_threshold = 3
# 성공적인 health check 횟수(연속적으로 건강한 것으로 간주되기 위함)
unhealthy_threshold = 5
# 실패한 health check 횟수(연속적으로 비건강한 것으로 간주되기 위함)
}
}
- path = "/": 로드밸런서가 서버의 루트 경로(index.html 등)에 요청을 보냄.
- matcher = "200-299": 서버가 응답으로 "200 OK" 같은 성공 메시지를 주면 살아있다고 판단
- interval = 5: 5초마다 한 번씩 확인
- timeout = 3: 3초 안에 응답이 없으면 이번 검사는 실패로 처리
- healthy_threshold = 3: 연속으로 3번 200 코드가 와야 트래픽을 다시 보내줌
- unhealthy_threshold = 5: 멀쩡하던 서버가 대답을 못 하거나 에러를 뱉으면 5번까지 기다리고 그 이상 실패하면 이 서버는 죽었다고 선언하고 명단에서 제외
resource "aws_lb_listener" "myhttp" {
load_balancer_arn = aws_lb.alb-web.arn
port = 80
protocol = "HTTP"
default_action {
# 'redirect'를 'forward'로 바꿔서 바로 Web 서버로 보냄.
type = "forward"
target_group_arn = aws_lb_target_group.tg-web.arn
}
}
# TG-WAS
resource "aws_lb_target_group" "tg-was" {
name = "ho-tg-was"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.vpc.id
health_check {
path = "/"
matcher = "200-299"
interval = 5
timeout = 3
healthy_threshold = 3
unhealthy_threshold = 5
}
}
resource "aws_lb_listener" "alb_listener-was" {
load_balancer_arn = aws_lb.alb-was.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.tg-was.arn
}
}
- load_balancer_arn: 이전에 만든 내부용 로드밸런서(alb-was)의 식별번호
- port = 80: Web 서버들이 이 로드밸런서를 찌를 때 사용하는 포트
- default_action:
- type = "forward": 들어온 요청을 아무 묻지도 따지지도 않고 바로 tg-was로 보냄
- 이미 위에서 검증됐기 때문이다.
6. Auto Scaling Group, Auto Scaling Group SG
# ASG-Web
resource "aws_autoscaling_group" "asg-web" {
name = "ho-asg-web"
desired_capacity = 2 # 항상 유지하고 싶은 목표 서버 대수
max_size = 4 # 트래픽이 폭주할 때 늘어날 수 있는 최대치
min_size = 2 # 아무리 트래픽이 없어도 유지할 최소치
target_group_arns = [aws_lb_target_group.tg-web.arn]
health_check_type = "EC2"
vpc_zone_identifier = [aws_subnet.web-sub1.id, aws_subnet.web-sub2.id]
tag {
key = "asg-web-key"
value = "asg-web-value"
propagate_at_launch = true
# ASG에서 생성된 EC2 인스턴스에 태그를 자동으로 적용할지에 대한 여부 지정
}
launch_template {
id = aws_launch_template.template-web.id
version = aws_launch_template.template-web.latest_version
}
instance_refresh {
strategy = "Rolling"
preferences {
min_healthy_percentage = 50
}
triggers = ["tag"]
# Terraform은 기본적으로 리소스의 설정이 바뀔 때만 변경 작업을 함.
# 그런데 외부 환경이나 코드 외의 조건에 따라 강제로 실행하고 싶을때가 있음.
# 이때 triggers를 써서 "이 값이 바뀌면 무조건 다시 실행하라"고 알려줌
}
}
- vpc_zone_identifier: 서버들이 태어날 주소지. 아까 만든 Private Subnet(web-sub1, 2)을 지정함. 외부에서 직접 접속은 못 하고 오직 ALB를 통해서만 들어오는 구조
- target_group_arns: 새로 태어난 서버들을 어느 타겟 그룹에 등록할지 정함. 그래야 로드밸런서가 새 서버를 인식하고 트래픽을 보낸다
- health_check_type = "EC2": EC2: 서버가 켜져 있는지만 확인
- version = aws_launch_template.template-web.latest_version: 항상 가장 최신 버전의 설계도를 사용하도록 설정. 테라폼에서 설계도를 수정하면 ASG가 알아서 최신본을 인지
- strategy = "Rolling": 한꺼번에 다 바꾸지 않고 하나씩 차례대로 새 서버로 교체
- min_healthy_percentage = 50: 교체 작업 중에도 전체 서버의 최소 50%는 항상 살아있어야 한다는 마지노선 (현재 2대 중 1대는 무조건 살아있어야 함)
- triggers = ["tag"]: 태그만 바뀌어도 서버를 새 버전으로 싹 갈아치우라는 신호
# ASG-Web-SG
resource "aws_security_group" "asg-sg-web" {
name = "ho-asg-sg-web"
description = "ASG Security Group"
vpc_id = aws_vpc.vpc.id
ingress {
description = "HTTP from ALB"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb-sg-web.id]
}
ingress {
description = "SSH From Anywhere or Your-IP"
# 원격으로 서버 접속해 SW 업데이트, 구성 변경 등
from_port = 22
to_port = 22
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 = "ho-asg-sg-web"
}
}
인바운드 규칙 1: ALB에서 오는 트래픽만 허용
- alb-sg-web의 ID를 넣음
- 오직 Web ALB를 거쳐서 검증된 패킷만 80번 포트로 들어와라는 뜻
인바운드 규칙 2: SSH 원격 접속 허용
- 관리자가 서버에 접속해서 설정을 바꾸거나 로그를 볼 수 있도록 22번 포트를 열어둠
아웃바운드 규칙 (Egress)
- 서버가 외부 인터넷에 자유롭게 연결할 수 있게 함
# ASG-WAS
resource "aws_autoscaling_group" "asg-was" {
name = "ho-asg-was"
desired_capacity = 2
max_size = 4
min_size = 2
target_group_arns = [aws_lb_target_group.tg-was.arn]
health_check_type = "EC2"
vpc_zone_identifier = [aws_subnet.was-sub1.id, aws_subnet.was-sub2.id]
tag {
key = "asg-app-key"
value = "asg-app-value"
propagate_at_launch = true
# ASG에서 생성된 EC2 인스턴스에 태그를 자동으로 적용할지에 대한 여부 지정
}
launch_template {
id = aws_launch_template.template-was.id
version = aws_launch_template.template-was.latest_version
}
instance_refresh {
strategy = "Rolling"
preferences {
min_healthy_percentage = 50
}
triggers = ["tag"]
}
}
# ASG-WAS-SG
resource "aws_security_group" "asg-sg-was" {
name = "ho-asg-sg-was"
description = "ASG Security Group"
vpc_id = aws_vpc.vpc.id
ingress {
description = "HTTP from ALB"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb-sg-was.id]
}
ingress {
description = "SSH from Web Tier"
from_port = 22
to_port = 22
protocol = "tcp"
# Web 서버 보안 그룹을 가진 인스턴스만 WAS로 SSH 접속 가능하도록 제한
security_groups = [aws_security_group.asg-sg-web.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "ho-asg-sg-was"
}
}
- security_groups = [aws_security_group.alb-sg-was.id]
- Internal ALB(WAS용 로드밸런서)를 거쳐서 온 요청만 허용
- SSH 접속 (22번 포트):
- Web 서버 보안 그룹을 가진 인스턴스만 WAS로 SSH 접속 가능하도록 제한
'AWS' 카테고리의 다른 글
| AWS 3-Tier 및 Data Pipeline 구축 With Terraform (5) (0) | 2026.03.14 |
|---|---|
| AWS 3-Tier 및 Data Pipeline 구축 With Terraform (4) (0) | 2026.03.14 |
| AWS 3-Tier 및 Data Pipeline 구축 With Terraform (2) (0) | 2026.03.13 |
| AWS 3-Tier 및 Data Pipeline 구축 With Terraform (1) (0) | 2026.03.12 |
| AWS PallyCon Architecture 분석 (1) | 2026.03.11 |