테라폼

4-7. 테라폼 - RDS MySQL 구현하기

pininini 2026. 5. 13. 15:43

테라폼 - 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으로 구현해본다. 민감정보와 운영 설정값을 어떻게 분리해서 관리할지 정리할 예정이다.