테라폼 - RDS MySQL 구현하기
DB Subnet Group, Security Group, 백업, 삭제 보호, 비밀번호 관리까지 함께 보기
이전 글에서는 Terraform으로 S3를 구현하는 방법을 정리했다.
이번 글에서는 AWS의 관리형 데이터베이스 서비스인 RDS MySQL을 Terraform으로 구현해보려 한다.
RDS는 EC2에 직접 MySQL을 설치하는 방식과 다르게, AWS가 데이터베이스 운영에 필요한 많은 부분을 관리해주는 서비스다.
DB 인스턴스 생성
스토리지 관리
백업
스냅샷
패치
모니터링
Multi-AZ 구성
하지만 Terraform으로 RDS를 만들 때는 단순히 aws_db_instance 하나만 작성하면 끝나는 것이 아니다.
RDS는 네트워크, 보안, 백업, 삭제 보호, 비밀번호 관리까지 함께 고려해야 한다.
RDS는 생성보다 삭제와 비밀번호 관리가 더 중요하다.
목차
- 1. RDS란 무엇인가
- 2. RDS를 만들 때 필요한 요소
- 3. RDS는 어디에 배치해야 할까?
- 4. DB Subnet Group 만들기
- 5. RDS Security Group 만들기
- 6. 기본 RDS MySQL 생성 코드
- 7. 비밀번호 관리 방식
- 8. 백업과 스냅샷 설정
- 9. 삭제 보호 설정
- 10. 실전 예제: Private RDS MySQL 구성
- 11. RDS 의존성 흐름
- 12. 자주 하는 실수
- 13. 마무리
1. RDS란 무엇인가
RDS는 Relational Database Service의 약자다.
AWS에서 제공하는 관리형 관계형 데이터베이스 서비스다.
직접 EC2에 MySQL을 설치해서 운영할 수도 있지만, 그렇게 하면 다음 작업을 모두 직접 관리해야 한다.
DB 설치
패치
백업
장애 대응
스토리지 확장
모니터링
복구
RDS를 사용하면 이런 운영 작업 중 상당 부분을 AWS 관리형 서비스로 처리할 수 있다.
RDS는 여러 데이터베이스 엔진을 지원한다.
MySQL
PostgreSQL
MariaDB
Oracle
SQL Server
Db2
이번 글에서는 초보자가 많이 사용하는 RDS MySQL 기준으로 설명한다.
2. RDS를 만들 때 필요한 요소
Terraform으로 RDS를 만들 때는 다음 요소들을 함께 생각해야 한다.
| 요소 | 설명 |
| DB Subnet Group | RDS가 배치될 Subnet 묶음 |
| Security Group | DB 접속을 허용할 네트워크 규칙 |
| DB Instance | 실제 RDS 인스턴스 |
| Username / Password | DB master 계정 정보 |
| Backup | 자동 백업 보존 기간 |
| Snapshot | 삭제 시 최종 백업 여부 |
| Deletion Protection | 실수로 DB가 삭제되지 않도록 보호 |
즉, RDS는 다음 리소스와 함께 구성하는 것이 일반적이다.
aws_db_subnet_group
aws_security_group
aws_vpc_security_group_ingress_rule
aws_db_instance
3. RDS는 어디에 배치해야 할까?
RDS는 일반적으로 Private Subnet에 배치하는 것이 좋다.
DB는 외부 인터넷에서 직접 접근할 필요가 거의 없다.
대부분의 경우 애플리케이션 서버만 DB에 접근하면 된다.
User
→ ALB
→ EC2 또는 ECS
→ RDS
따라서 RDS는 Public Subnet이 아니라 Private Subnet에 배치하고, Security Group으로 접근 대상을 제한하는 것이 안전하다.
권장 구조:
App Security Group → RDS Security Group → 3306 포트
반대로 다음 구조는 피하는 것이 좋다.
Internet → RDS 3306 직접 접근
초보자 실습 중에도 RDS를 public으로 열어두는 습관은 좋지 않다.
RDS는 기본적으로 Private Subnet에 두고, 애플리케이션 Security Group에서만 접근하도록 제한하는 것이 좋다.
4. DB Subnet Group 만들기
RDS는 EC2처럼 단일 Subnet 하나를 직접 지정하지 않는다.
대신 DB Subnet Group을 사용한다.
DB Subnet Group은 RDS가 사용할 Subnet 목록을 묶어둔 리소스다.
Private Subnet A
Private Subnet C
→ DB Subnet Group
→ RDS
Terraform 코드는 다음과 같다.
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-db-subnet-group"
subnet_ids = var.private_subnet_ids
tags = merge(local.common_tags, {
Name = "${var.project_name}-db-subnet-group"
})
}
여기서 private_subnet_ids는 Private Subnet ID 목록이다.
variable "private_subnet_ids" {
description = "Private subnet IDs for RDS"
type = list(string)
}
운영 환경에서는 가능하면 서로 다른 AZ의 Private Subnet을 두 개 이상 사용하는 것이 좋다.
ap-northeast-2a private subnet
ap-northeast-2c private subnet
이렇게 하면 Multi-AZ 구성이나 장애 대응 측면에서 더 유리하다.
5. RDS Security Group 만들기
RDS Security Group은 DB에 누가 접근할 수 있는지를 제어한다.
MySQL은 기본적으로 3306 포트를 사용한다.
RDS를 전체 인터넷에 열어두면 위험하다.
# 권장하지 않음
0.0.0.0/0 → RDS 3306
대신 애플리케이션 Security Group에서만 접근하도록 제한한다.
App Security Group
→ RDS Security Group
→ 3306 포트
Terraform 코드는 다음과 같다.
resource "aws_security_group" "rds" {
name = "${var.project_name}-rds-sg"
description = "Security group for RDS MySQL"
vpc_id = var.vpc_id
tags = merge(local.common_tags, {
Name = "${var.project_name}-rds-sg"
})
}
resource "aws_vpc_security_group_ingress_rule" "rds_from_app" {
security_group_id = aws_security_group.rds.id
referenced_security_group_id = var.app_security_group_id
ip_protocol = "tcp"
from_port = 3306
to_port = 3306
description = "Allow MySQL access from app security group"
}
resource "aws_vpc_security_group_egress_rule" "rds_all" {
security_group_id = aws_security_group.rds.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
description = "Allow all outbound traffic"
}
여기서 중요한 부분은 referenced_security_group_id다.
referenced_security_group_id = var.app_security_group_id
이 설정은 특정 IP가 아니라 App Security Group을 가진 리소스만 RDS에 접근할 수 있도록 한다.
즉, IP 중심이 아니라 역할 중심의 보안 규칙이다.
6. 기본 RDS MySQL 생성 코드
이제 기본적인 RDS MySQL 인스턴스를 만들어보자.
resource "aws_db_instance" "mysql" {
identifier = "${var.project_name}-mysql"
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
allocated_storage = 20
max_allocated_storage = 100
storage_type = "gp3"
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = var.db_password
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
publicly_accessible = false
backup_retention_period = 7
deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "${var.project_name}-mysql-final-snapshot"
tags = merge(local.common_tags, {
Name = "${var.project_name}-mysql"
})
}
주요 설정을 정리하면 다음과 같다.
| 설정 | 의미 |
| engine | DB 엔진 |
| engine_version | DB 엔진 버전 |
| instance_class | DB 인스턴스 사양 |
| allocated_storage | 초기 스토리지 크기 |
| max_allocated_storage | 자동 스토리지 확장 최대값 |
| storage_encrypted | 스토리지 암호화 여부 |
| db_subnet_group_name | RDS가 배치될 Subnet Group |
| vpc_security_group_ids | RDS에 연결할 Security Group |
| publicly_accessible | 외부 공개 여부 |
| backup_retention_period | 자동 백업 보존 기간 |
| deletion_protection | 삭제 보호 여부 |
| skip_final_snapshot | 삭제 시 최종 스냅샷 생략 여부 |
7. 비밀번호 관리 방식
RDS를 만들 때 가장 조심해야 하는 부분 중 하나가 비밀번호다.
초보자는 다음처럼 변수로 비밀번호를 넣기 쉽다.
variable "db_password" {
description = "Database password"
type = string
sensitive = true
}
그리고 RDS에서 사용한다.
password = var.db_password
이 방식은 간단하지만 주의할 점이 있다.
Terraform 변수에 sensitive = true를 붙여도 값이 state에 저장될 수 있다.
즉, CLI 출력에서는 숨겨져도 Terraform state에는 민감한 값이 남을 수 있다.
따라서 운영 환경에서는 RDS master password를 코드나 tfvars에 직접 넣는 방식은 피하는 것이 좋다.
7.1 방법 1: Terraform 변수로 비밀번호 전달
학습용 또는 간단한 실습에서는 변수로 비밀번호를 전달할 수 있다.
variable "db_password" {
description = "Database master password"
type = string
sensitive = true
}
resource "aws_db_instance" "mysql" {
username = var.db_username
password = var.db_password
}
다만 이 방식은 운영 환경에서는 신중해야 한다.
장점:
- 단순하다.
- 이해하기 쉽다.
단점:
- state에 민감정보가 남을 수 있다.
- tfvars 관리에 주의해야 한다.
- 비밀번호 회전 관리가 어렵다.
7.2 방법 2: RDS가 Secrets Manager로 master password 관리
최근에는 RDS가 master user password를 AWS Secrets Manager로 관리하도록 설정할 수 있다.
Terraform에서는 다음처럼 사용할 수 있다.
resource "aws_db_instance" "mysql" {
identifier = "${var.project_name}-mysql"
engine = "mysql"
instance_class = "db.t3.micro"
allocated_storage = 20
db_name = var.db_name
username = var.db_username
manage_master_user_password = true
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
publicly_accessible = false
}
이 방식에서는 Terraform 코드에 master password를 직접 넣지 않는다.
password 설정 X
manage_master_user_password = true
RDS가 Secrets Manager에 Secret을 생성하고 master password를 관리한다.
이 방식은 운영 환경에서 더 안전한 선택이 될 수 있다.
장점:
- Terraform 코드에 DB password를 직접 넣지 않는다.
- RDS와 Secrets Manager를 통해 credential을 관리할 수 있다.
- 비밀번호 회전과 관리 측면에서 유리하다.
주의:
- Secrets Manager 비용이 발생할 수 있다.
- 애플리케이션이 Secret을 읽으려면 IAM 권한이 필요하다.
7.3 애플리케이션에서 비밀번호 읽기
RDS 비밀번호를 Secrets Manager에서 관리한다면 애플리케이션은 Secret을 읽을 권한이 필요하다.
예를 들어 ECS Task나 EC2 Role에 다음 권한이 필요할 수 있다.
secretsmanager:GetSecretValue
즉, RDS 접속에는 세 가지가 함께 필요하다.
1. 네트워크 접근
App Security Group → RDS Security Group 3306
2. Secret 접근 권한
IAM Role → secretsmanager:GetSecretValue
3. DB 로그인 정보
username / password
Security Group만 맞다고 DB 접속이 되는 것이 아니고, IAM 권한만 있다고 DB 포트에 접근할 수 있는 것도 아니다.
8. 백업과 스냅샷 설정
RDS는 데이터베이스이기 때문에 백업 설정이 매우 중요하다.
Terraform에서 자동 백업 보존 기간은 다음 설정으로 지정할 수 있다.
backup_retention_period = 7
이 값은 자동 백업을 며칠 동안 보관할지 의미한다.
0 → 자동 백업 비활성화
7 → 7일간 자동 백업 보관
30 → 30일간 자동 백업 보관
운영 환경에서는 일반적으로 0으로 두지 않는 것이 좋다.
8.1 삭제 시 final snapshot
RDS를 삭제할 때는 최종 스냅샷을 남길지 결정해야 한다.
skip_final_snapshot = false
이 설정은 삭제 시 최종 스냅샷을 생략하지 않겠다는 의미다.
이 경우 final snapshot identifier도 지정해야 한다.
final_snapshot_identifier = "${var.project_name}-mysql-final-snapshot"
반대로 학습용 RDS에서는 빠르게 삭제하기 위해 다음처럼 설정할 수 있다.
skip_final_snapshot = true
하지만 운영 환경에서는 매우 위험할 수 있다.
학습용:
skip_final_snapshot = true 가능
운영:
skip_final_snapshot = false 권장
RDS를 삭제할 때 final snapshot을 남기지 않으면 삭제 후 복구할 수 있는 지점이 줄어든다.
8.2 backup_window와 maintenance_window
운영 환경에서는 백업 시간과 유지보수 시간도 고려해야 한다.
backup_window = "18:00-19:00"
maintenance_window = "sun:19:00-sun:20:00"
UTC 기준으로 설정되므로 한국 시간과 차이를 고려해야 한다.
예를 들어 한국 시간 새벽에 백업이 돌도록 하고 싶다면 UTC 변환을 고려해야 한다.
9. 삭제 보호 설정
RDS는 실수로 삭제되면 큰 문제가 생길 수 있다.
그래서 삭제 보호를 적극적으로 고려해야 한다.
9.1 deletion_protection
RDS 자체에는 deletion_protection 설정이 있다.
deletion_protection = true
이 설정이 켜져 있으면 RDS 삭제가 보호된다.
운영 RDS에서는 가능한 켜두는 것이 좋다.
9.2 Terraform lifecycle prevent_destroy
Terraform 레벨에서도 삭제를 막을 수 있다.
resource "aws_db_instance" "mysql" {
identifier = "${var.project_name}-mysql"
lifecycle {
prevent_destroy = true
}
}
이 설정은 Terraform이 해당 리소스를 삭제하려고 할 때 오류를 발생시킨다.
즉, RDS 보호는 두 단계로 생각할 수 있다.
AWS RDS 레벨:
deletion_protection = true
Terraform 레벨:
prevent_destroy = true
운영 환경에서는 둘 다 고려할 수 있다.
중요한 DB는 AWS 서비스 레벨과 Terraform 레벨에서 모두 삭제를 방지하는 것이 좋다.
10. 실전 예제: Private RDS MySQL 구성
이제 실전형 예제를 하나로 정리해보자.
구조는 다음과 같다.
Private Subnet
→ DB Subnet Group
→ RDS MySQL
App Security Group
→ RDS Security Group 3306 허용
10.1 variables.tf
variable "project_name" {
description = "Project name"
type = string
}
variable "vpc_id" {
description = "VPC ID"
type = string
}
variable "private_subnet_ids" {
description = "Private subnet IDs for RDS"
type = list(string)
}
variable "app_security_group_id" {
description = "Application security group ID"
type = string
}
variable "db_name" {
description = "Database name"
type = string
}
variable "db_username" {
description = "Database master username"
type = string
}
variable "db_instance_class" {
description = "RDS instance class"
type = string
default = "db.t3.micro"
}
variable "db_allocated_storage" {
description = "RDS allocated storage"
type = number
default = 20
}
10.2 terraform.tfvars
project_name = "demo"
vpc_id = "vpc-xxxxxxxx"
private_subnet_ids = [
"subnet-aaaaaaaa",
"subnet-bbbbbbbb"
]
app_security_group_id = "sg-xxxxxxxx"
db_name = "appdb"
db_username = "admin"
실제 프로젝트에서는 vpc_id, private_subnet_ids, app_security_group_id를 직접 쓰기보다 network state output에서 가져올 수 있다.
10.3 locals.tf
locals {
common_tags = {
Project = var.project_name
ManagedBy = "terraform"
}
}
10.4 main.tf
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-db-subnet-group"
subnet_ids = var.private_subnet_ids
tags = merge(local.common_tags, {
Name = "${var.project_name}-db-subnet-group"
})
}
resource "aws_security_group" "rds" {
name = "${var.project_name}-rds-sg"
description = "Security group for RDS MySQL"
vpc_id = var.vpc_id
tags = merge(local.common_tags, {
Name = "${var.project_name}-rds-sg"
})
}
resource "aws_vpc_security_group_ingress_rule" "rds_from_app" {
security_group_id = aws_security_group.rds.id
referenced_security_group_id = var.app_security_group_id
ip_protocol = "tcp"
from_port = 3306
to_port = 3306
description = "Allow MySQL access from app security group"
}
resource "aws_vpc_security_group_egress_rule" "rds_all" {
security_group_id = aws_security_group.rds.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
description = "Allow all outbound traffic"
}
resource "aws_db_instance" "mysql" {
identifier = "${var.project_name}-mysql"
engine = "mysql"
engine_version = "8.0"
instance_class = var.db_instance_class
allocated_storage = var.db_allocated_storage
max_allocated_storage = 100
storage_type = "gp3"
storage_encrypted = true
db_name = var.db_name
username = var.db_username
manage_master_user_password = true
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
publicly_accessible = false
backup_retention_period = 7
backup_window = "18:00-19:00"
maintenance_window = "sun:19:00-sun:20:00"
deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "${var.project_name}-mysql-final-snapshot"
auto_minor_version_upgrade = true
copy_tags_to_snapshot = true
lifecycle {
prevent_destroy = true
}
tags = merge(local.common_tags, {
Name = "${var.project_name}-mysql"
})
}
이 예제의 특징은 다음과 같다.
Private Subnet 배치
RDS Security Group 분리
App Security Group에서만 3306 접근 허용
Public 접근 비활성화
Storage 암호화
자동 백업 7일
삭제 보호 활성화
Final Snapshot 생성
Terraform prevent_destroy 활성화
RDS가 master password를 Secrets Manager로 관리
10.5 outputs.tf
output "rds_endpoint" {
description = "RDS endpoint"
value = aws_db_instance.mysql.endpoint
}
output "rds_address" {
description = "RDS address"
value = aws_db_instance.mysql.address
}
output "rds_port" {
description = "RDS port"
value = aws_db_instance.mysql.port
}
output "rds_security_group_id" {
description = "RDS security group ID"
value = aws_security_group.rds.id
}
output "rds_master_user_secret_arn" {
description = "RDS managed master user secret ARN"
value = aws_db_instance.mysql.master_user_secret[0].secret_arn
}
rds_endpoint는 애플리케이션에서 DB 접속 주소로 사용할 수 있다.
다만 master password를 Secrets Manager로 관리하는 경우 애플리케이션이 Secret을 읽을 수 있는 IAM 권한도 함께 필요하다.
11. RDS 의존성 흐름
RDS는 여러 리소스를 참조한다.
VPC
├── Private Subnet
│ └── DB Subnet Group
│ └── RDS
└── Security Group
└── RDS
조금 더 정확히 보면 RDS는 DB Subnet Group과 Security Group을 함께 참조한다.

이 구조를 보면 RDS가 단독으로 만들어지는 리소스가 아니라는 것을 알 수 있다.
네트워크와 보안, 비밀번호 관리가 함께 연결되어야 실제로 사용할 수 있다.
12. 자주 하는 실수
12.1 RDS를 Public으로 열어둠
RDS를 인터넷에서 직접 접근 가능하게 만드는 것은 위험하다.
publicly_accessible = true
특별한 이유가 없다면 RDS는 Private Subnet에 두고 다음처럼 설정하는 것이 좋다.
publicly_accessible = false
12.2 RDS Security Group을 0.0.0.0/0으로 열어둠
MySQL 3306 포트를 전체 인터넷에 열면 매우 위험하다.
# 권장하지 않음
0.0.0.0/0 → 3306
대신 App Security Group에서만 접근하도록 제한한다.
App Security Group → RDS Security Group → 3306
12.3 비밀번호를 terraform.tfvars에 평문으로 저장
DB password를 tfvars에 직접 넣는 것은 위험할 수 있다.
db_password = "my-secret-password"
운영 환경에서는 RDS의 manage_master_user_password 기능이나 Secrets Manager를 사용하는 것이 좋다.
12.4 sensitive = true면 완전히 안전하다고 생각함
Terraform variable에 sensitive = true를 붙이면 CLI 출력에서는 숨겨질 수 있다.
하지만 값이 state에 저장될 수 있다는 점은 여전히 주의해야 한다.
sensitive = true
→ 화면 출력 숨김
state 저장 가능성
→ 별도 관리 필요
12.5 skip_final_snapshot = true를 운영에 사용
학습용 RDS는 삭제를 쉽게 하기 위해 final snapshot을 생략할 수 있다.
skip_final_snapshot = true
하지만 운영 DB에서는 삭제 전 최종 스냅샷을 남기는 것이 안전하다.
skip_final_snapshot = false
12.6 deletion_protection을 끄고 운영함
운영 DB는 실수로 삭제되면 치명적이다.
가능하면 삭제 보호를 켜는 것이 좋다.
deletion_protection = true
또한 Terraform의 prevent_destroy도 함께 고려할 수 있다.
12.7 backup_retention_period를 0으로 둠
backup_retention_period = 0은 자동 백업을 비활성화하는 의미로 사용될 수 있다.
운영 환경에서는 자동 백업을 반드시 고려해야 한다.
backup_retention_period = 7
12.8 RDS 생성 직후 바로 애플리케이션을 실행함
RDS는 Terraform apply가 끝났다고 해서 애플리케이션이 즉시 안정적으로 접속 가능한 상태라고 단정하기 어렵다.
RDS 상태가 available인지 확인하고, 네트워크와 Secret 권한까지 확인한 뒤 애플리케이션을 실행하는 것이 좋다.
RDS 생성
→ available 상태 확인
→ Secret 접근 확인
→ App 실행
12.9 RDS 비용을 잊음
RDS는 EC2처럼 실행 중이면 비용이 발생한다.
또한 스토리지, 백업, 스냅샷에도 비용이 발생할 수 있다.
학습용으로 생성했다면 실습 후 반드시 정리해야 한다.
단, 삭제 보호나 prevent_destroy를 켜두었다면 먼저 해당 설정을 해제해야 destroy할 수 있다.
13. 마무리
이번 글에서는 Terraform으로 RDS MySQL을 구현하는 방법을 정리했다.
RDS는 단순히 DB 인스턴스 하나를 만드는 것이 아니다.
다음 요소를 함께 고려해야 한다.
Private Subnet
DB Subnet Group
Security Group
Backup
Snapshot
Deletion Protection
Password Management
Secrets Manager
Terraform prevent_destroy
특히 RDS는 삭제되면 큰 문제가 생길 수 있는 리소스다.
따라서 운영 환경에서는 삭제 보호와 백업, final snapshot을 반드시 고려하는 것이 좋다.
또한 DB password를 Terraform 코드나 tfvars에 직접 넣기보다, Secrets Manager를 활용하는 구조가 더 안전하다.
한 줄 정리
RDS는 생성보다 네트워크 제한, 백업, 삭제 보호, 비밀번호 관리가 더 중요하다.
다음 글에서는 Secrets Manager와 SSM Parameter Store를 Terraform으로 구현해본다. 민감정보와 운영 설정값을 어떻게 분리해서 관리할지 정리할 예정이다.
'테라폼' 카테고리의 다른 글
| 4-9. 테라폼 - ALB와 Target Group 구현하기 (0) | 2026.05.14 |
|---|---|
| 4-8. 테라폼 - Secrets Manager와 SSM Parameter Store 구현하기 (0) | 2026.05.13 |
| 4-6. 테라폼 - S3 구현하기 (0) | 2026.05.13 |
| 4-5. 테라폼 - EC2 구현하기 (0) | 2026.05.12 |
| 4-4. 테라폼 - IAM Role과 Policy 구현하기 (0) | 2026.05.12 |