Skip to content

Commit c3bcc8f

Browse files
authored
feat: DB EC2의 백업 관리용 S3 버킷 생성 (#69)
1 parent 5f40744 commit c3bcc8f

10 files changed

Lines changed: 231 additions & 14 deletions

File tree

‎.github/workflows/terraform-apply.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ jobs:
199199
run: |
200200
terraform apply -auto-approve \
201201
-var-file="../../config/secrets/prod.tfvars" \
202+
-var-file="../../config/secrets/prod_db.tfvars" \
202203
-var-file="../../config/secrets/app_stack.tfvars"
203204
- name: Stop SSM Tunnel
204205
if: always()

‎.github/workflows/terraform-plan.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ jobs:
267267
run: |
268268
terraform plan -no-color \
269269
-var-file="../../config/secrets/prod.tfvars" \
270+
-var-file="../../config/secrets/prod_db.tfvars" \
270271
-var-file="../../config/secrets/app_stack.tfvars" \
271272
2>&1 | tee plan_output.txt
272273
echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ modules/shared_resources/dist/*.zip
1515
# --- IDE & OS ---
1616
.idea/
1717
.DS_Store
18+
19+
# --- AGENTS ---
20+
AGENTS.local.md

‎config/secrets‎

‎environment/prod/main.tf‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ module "prod_stack" {
2929
db_data_volume_size = var.db_data_volume_size
3030

3131
# 보안 그룹 규칙
32-
api_ingress_rules = var.api_ingress_rules
33-
db_ingress_rules = var.db_ingress_rules
32+
api_ingress_rules = var.api_ingress_rules
33+
rds_ingress_rules = var.rds_ingress_rules
34+
db_ec2_ingress_rules = var.db_ec2_ingress_rules
3435

3536
# RDS 식별자 설정
3637
rds_identifier = var.rds_identifier

‎environment/prod/mysql_backup.tf‎

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
resource "aws_s3_bucket" "mysql_backup" {
2+
bucket = var.mysql_backup_bucket_name
3+
force_destroy = false
4+
object_lock_enabled = true
5+
6+
tags = {
7+
Name = var.mysql_backup_bucket_name
8+
}
9+
10+
lifecycle {
11+
prevent_destroy = true
12+
}
13+
}
14+
15+
resource "aws_s3_bucket_ownership_controls" "mysql_backup" {
16+
bucket = aws_s3_bucket.mysql_backup.id
17+
18+
rule {
19+
object_ownership = "BucketOwnerEnforced"
20+
}
21+
}
22+
23+
resource "aws_s3_bucket_public_access_block" "mysql_backup" {
24+
bucket = aws_s3_bucket.mysql_backup.id
25+
26+
block_public_acls = true
27+
block_public_policy = true
28+
ignore_public_acls = true
29+
restrict_public_buckets = true
30+
}
31+
32+
resource "aws_s3_bucket_policy" "mysql_backup" {
33+
bucket = aws_s3_bucket.mysql_backup.id
34+
35+
policy = jsonencode({
36+
Version = "2012-10-17"
37+
Statement = [
38+
{
39+
Sid = "DenyInsecureTransport"
40+
Effect = "Deny"
41+
Principal = "*"
42+
Action = "s3:*"
43+
Resource = [
44+
aws_s3_bucket.mysql_backup.arn,
45+
"${aws_s3_bucket.mysql_backup.arn}/*",
46+
]
47+
Condition = {
48+
Bool = {
49+
"aws:SecureTransport" = "false"
50+
}
51+
}
52+
},
53+
]
54+
})
55+
}
56+
57+
resource "aws_s3_bucket_server_side_encryption_configuration" "mysql_backup" {
58+
bucket = aws_s3_bucket.mysql_backup.id
59+
60+
rule {
61+
apply_server_side_encryption_by_default {
62+
sse_algorithm = "AES256"
63+
}
64+
}
65+
}
66+
67+
resource "aws_s3_bucket_versioning" "mysql_backup" {
68+
bucket = aws_s3_bucket.mysql_backup.id
69+
70+
versioning_configuration {
71+
status = "Enabled"
72+
}
73+
}
74+
75+
resource "aws_s3_bucket_object_lock_configuration" "mysql_backup" {
76+
bucket = aws_s3_bucket.mysql_backup.id
77+
78+
depends_on = [aws_s3_bucket_versioning.mysql_backup]
79+
80+
rule {
81+
default_retention {
82+
mode = "GOVERNANCE"
83+
days = 14
84+
}
85+
}
86+
}
87+
88+
resource "aws_s3_bucket_lifecycle_configuration" "mysql_backup" {
89+
bucket = aws_s3_bucket.mysql_backup.id
90+
91+
depends_on = [
92+
aws_s3_bucket_versioning.mysql_backup,
93+
aws_s3_bucket_object_lock_configuration.mysql_backup,
94+
]
95+
96+
rule {
97+
id = "expire-mysql-backups-after-14-days"
98+
status = "Enabled"
99+
100+
filter {}
101+
102+
expiration {
103+
days = 14
104+
}
105+
106+
noncurrent_version_expiration {
107+
noncurrent_days = 1
108+
}
109+
110+
abort_incomplete_multipart_upload {
111+
days_after_initiation = 1
112+
}
113+
}
114+
115+
rule {
116+
id = "remove-expired-delete-markers"
117+
status = "Enabled"
118+
119+
filter {}
120+
121+
expiration {
122+
expired_object_delete_marker = true
123+
}
124+
}
125+
}
126+
127+
# S3 Gateway Endpoint에는 별도 시간당 또는 데이터 처리 비용이 없습니다.
128+
# 접근 권한은 Terraform에 선언하지 않고 수동으로 관리하는 IAM 정책에서 제한합니다.
129+
data "aws_route_table" "db_ec2" {
130+
subnet_id = var.db_ec2_subnet_id
131+
}
132+
133+
resource "aws_vpc_endpoint" "mysql_backup_s3" {
134+
vpc_id = data.aws_vpc.default.id
135+
service_name = "com.amazonaws.ap-northeast-2.s3"
136+
vpc_endpoint_type = "Gateway"
137+
route_table_ids = [data.aws_route_table.db_ec2.id]
138+
139+
policy = jsonencode({
140+
Version = "2012-10-17"
141+
Statement = [
142+
{
143+
Sid = "AllowBackupBucketMetadataAccess"
144+
Effect = "Allow"
145+
Principal = "*"
146+
Action = [
147+
"s3:GetBucketLocation",
148+
"s3:ListBucket",
149+
"s3:ListBucketMultipartUploads",
150+
"s3:ListBucketVersions",
151+
]
152+
Resource = aws_s3_bucket.mysql_backup.arn
153+
},
154+
{
155+
Sid = "AllowBackupObjectAccess"
156+
Effect = "Allow"
157+
Principal = "*"
158+
Action = [
159+
"s3:AbortMultipartUpload",
160+
"s3:GetObject",
161+
"s3:GetObjectVersion",
162+
"s3:ListMultipartUploadParts",
163+
"s3:PutObject",
164+
]
165+
Resource = "${aws_s3_bucket.mysql_backup.arn}/*"
166+
},
167+
]
168+
})
169+
170+
tags = {
171+
Name = "solid-connection-prod-mysql-backup-s3"
172+
}
173+
}

‎environment/prod/outputs.tf‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
output "mysql_backup_bucket_name" {
2+
description = "Prod MySQL 백업 S3 버킷 이름"
3+
value = aws_s3_bucket.mysql_backup.bucket
4+
}
5+
6+
output "mysql_backup_s3_vpc_endpoint_id" {
7+
description = "Prod MySQL 백업용 S3 Gateway VPC Endpoint ID"
8+
value = aws_vpc_endpoint.mysql_backup_s3.id
9+
}

‎environment/prod/variables.tf‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ variable "db_data_volume_size" {
3838
type = number
3939
}
4040

41+
variable "mysql_backup_bucket_name" {
42+
description = "Prod MySQL dump와 binlog를 보관할 S3 버킷 이름"
43+
type = string
44+
}
45+
4146
variable "api_ingress_rules" {
4247
description = "List of ingress rules for API Server"
4348
type = list(object({
@@ -49,8 +54,18 @@ variable "api_ingress_rules" {
4954
}))
5055
}
5156

52-
variable "db_ingress_rules" {
53-
description = "List of ingress rules for DB Server"
57+
variable "rds_ingress_rules" {
58+
description = "API Server Security Group에서 RDS로 허용할 ingress 규칙"
59+
type = list(object({
60+
from_port = number
61+
to_port = number
62+
protocol = string
63+
description = string
64+
}))
65+
}
66+
67+
variable "db_ec2_ingress_rules" {
68+
description = "API Server Security Group에서 DB EC2로 허용할 ingress 규칙"
5469
type = list(object({
5570
from_port = number
5671
to_port = number

‎modules/app_stack/security_groups.tf‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,15 @@ resource "aws_security_group" "db_ec2_sg" {
3535
description = "Security Group for DB EC2"
3636
vpc_id = var.vpc_id
3737

38-
ingress {
39-
description = "MySQL from API server"
40-
from_port = 3306
41-
to_port = 3306
42-
protocol = "tcp"
43-
security_groups = [aws_security_group.api_sg.id]
38+
dynamic "ingress" {
39+
for_each = var.db_ec2_ingress_rules
40+
content {
41+
description = ingress.value.description
42+
from_port = ingress.value.from_port
43+
to_port = ingress.value.to_port
44+
protocol = ingress.value.protocol
45+
security_groups = [aws_security_group.api_sg.id]
46+
}
4447
}
4548

4649
egress {
@@ -63,7 +66,7 @@ resource "aws_security_group" "db_sg" {
6366
vpc_id = var.vpc_id
6467

6568
dynamic "ingress" {
66-
for_each = var.db_ingress_rules
69+
for_each = var.rds_ingress_rules
6770
content {
6871
description = ingress.value.description
6972
from_port = ingress.value.from_port

‎modules/app_stack/variables.tf‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,19 @@ variable "api_ingress_rules" {
7474
}))
7575
}
7676

77-
variable "db_ingress_rules" {
78-
description = "List of ingress rules for DB Server"
77+
variable "rds_ingress_rules" {
78+
description = "API Server Security Group에서 RDS로 허용할 ingress 규칙"
79+
type = list(object({
80+
from_port = number
81+
to_port = number
82+
protocol = string
83+
description = string
84+
}))
85+
default = []
86+
}
87+
88+
variable "db_ec2_ingress_rules" {
89+
description = "API Server Security Group에서 DB EC2로 허용할 ingress 규칙"
7990
type = list(object({
8091
from_port = number
8192
to_port = number

0 commit comments

Comments
 (0)