From 9a21071f99da7db77dd9163a74bccdd6125cae07 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Tue, 23 Jun 2026 00:33:44 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20prod=20=ED=99=98=EA=B2=BD=EC=9D=98?= =?UTF-8?q?=20db=5Fec2=20=EC=9E=90=EC=9B=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- environment/prod/main.tf | 5 ++ environment/prod/variables.tf | 10 +++ modules/app_stack/db_ec2.tf | 118 +++++++++++++++++++++++++++ modules/app_stack/output.tf | 9 ++ modules/app_stack/security_groups.tf | 29 ++++++- modules/app_stack/variables.tf | 18 ++++ 6 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 modules/app_stack/db_ec2.tf diff --git a/environment/prod/main.tf b/environment/prod/main.tf index 960ead5..0accfb4 100644 --- a/environment/prod/main.tf +++ b/environment/prod/main.tf @@ -21,6 +21,11 @@ module "prod_stack" { instance_type = var.server_instance_type db_instance_class = var.db_instance_class + # DB EC2 설정 + enable_db_ec2 = true + db_instance_type = var.db_ec2_instance_type + db_ami_id = var.db_ec2_ami_id + # 보안 그룹 규칙 api_ingress_rules = var.api_ingress_rules db_ingress_rules = var.db_ingress_rules diff --git a/environment/prod/variables.tf b/environment/prod/variables.tf index c81bcf2..83a608f 100644 --- a/environment/prod/variables.tf +++ b/environment/prod/variables.tf @@ -18,6 +18,16 @@ variable "db_instance_class" { type = string } +variable "db_ec2_instance_type" { + description = "DB EC2 인스턴스 타입" + type = string +} + +variable "db_ec2_ami_id" { + description = "DB EC2에 사용할 커스텀 AMI ID" + type = string +} + variable "api_ingress_rules" { description = "List of ingress rules for API Server" type = list(object({ diff --git a/modules/app_stack/db_ec2.tf b/modules/app_stack/db_ec2.tf new file mode 100644 index 0000000..ac12290 --- /dev/null +++ b/modules/app_stack/db_ec2.tf @@ -0,0 +1,118 @@ +data "cloudinit_config" "db_init" { + count = var.enable_db_ec2 ? 1 : 0 + gzip = true + base64_encode = true + + part { + content_type = "text/x-shellscript" + content = templatefile("${path.module}/scripts/mysql_setup.sh.tftpl", { + db_root_username_b64 = base64encode(var.db_username) + db_root_password_b64 = base64encode(var.db_password) + mysql_config_content = file("${path.module}/templates/mysql_tuning.cnf") + }) + filename = "mysql_setup.sh" + } +} + +locals { + mysql_tuning_config_b64 = base64encode(file("${path.module}/templates/mysql_tuning.cnf")) + + mysql_tuning_ssm_params = jsonencode({ + commands = [ + "cloud-init status --wait > /dev/null", + "sudo mkdir -p /etc/mysql/conf.d", + "echo ${local.mysql_tuning_config_b64} | base64 -d | sudo tee /etc/mysql/conf.d/tuning.cnf > /dev/null", + "sudo chmod 644 /etc/mysql/conf.d/tuning.cnf", + "sudo docker restart mysql-server", + "for i in $(seq 1 30); do if sudo docker exec mysql-server mysqladmin ping --silent >/dev/null 2>&1; then exit 0; fi; sleep 2; done; sudo docker logs --tail 100 mysql-server >&2; exit 1", + ] + executionTimeout = ["600"] + }) +} + +resource "aws_instance" "db_server" { + count = var.enable_db_ec2 ? 1 : 0 + + ami = var.db_ami_id + instance_type = var.db_instance_type + + vpc_security_group_ids = [aws_security_group.db_ec2_sg[count.index].id] + associate_public_ip_address = false + iam_instance_profile = var.ec2_iam_instance_profile + key_name = var.key_name + + user_data_base64 = data.cloudinit_config.db_init[count.index].rendered + + metadata_options { + http_endpoint = "enabled" + http_tokens = "required" + http_put_response_hop_limit = 1 + } + + root_block_device { + volume_size = 20 + volume_type = "gp3" + encrypted = true + delete_on_termination = true + } + + tags = { + Name = "solid-connection-db-mysql-${var.env_name}" + } + + user_data_replace_on_change = false + + lifecycle { + ignore_changes = [ + user_data, + user_data_base64, + user_data_replace_on_change, + key_name, + ] + } +} + +resource "null_resource" "update_mysql_tuning" { + count = var.enable_db_ec2 ? 1 : 0 + depends_on = [aws_instance.db_server] + + triggers = { + config_hash = sha256(file("${path.module}/templates/mysql_tuning.cnf")) + instance_id = aws_instance.db_server[count.index].id + } + + provisioner "local-exec" { + interpreter = ["bash", "-c"] + command = <<-EOT + set -euo pipefail + INSTANCE_ID='${aws_instance.db_server[count.index].id}' + COMMAND_ID=$(aws ssm send-command \ + --instance-ids "$INSTANCE_ID" \ + --document-name "AWS-RunShellScript" \ + --parameters '${local.mysql_tuning_ssm_params}' \ + --output text \ + --query "Command.CommandId") + ATTEMPTS=0 + while [ "$ATTEMPTS" -lt 60 ]; do + STATUS=$(aws ssm get-command-invocation \ + --command-id "$COMMAND_ID" \ + --instance-id "$INSTANCE_ID" \ + --query "Status" --output text 2>/dev/null || echo "Pending") + case "$STATUS" in + Success) exit 0 ;; + Failed|Cancelled|TimedOut|Undeliverable) + echo "SSM command $STATUS" >&2 + aws ssm get-command-invocation \ + --command-id "$COMMAND_ID" \ + --instance-id "$INSTANCE_ID" \ + --query "StandardErrorContent" --output text >&2 + exit 1 ;; + esac + ATTEMPTS=$((ATTEMPTS + 1)) + sleep 10 + done + echo "SSM command timed out after 600s" >&2 + exit 1 + EOT + } +} diff --git a/modules/app_stack/output.tf b/modules/app_stack/output.tf index e69de29..523c77a 100644 --- a/modules/app_stack/output.tf +++ b/modules/app_stack/output.tf @@ -0,0 +1,9 @@ +output "db_server_private_ip" { + description = "DB EC2 서버 private IP" + value = try(aws_instance.db_server[0].private_ip, null) +} + +output "db_server_instance_id" { + description = "DB EC2 서버 인스턴스 ID" + value = try(aws_instance.db_server[0].id, null) +} diff --git a/modules/app_stack/security_groups.tf b/modules/app_stack/security_groups.tf index 1a10fbd..7474ed7 100644 --- a/modules/app_stack/security_groups.tf +++ b/modules/app_stack/security_groups.tf @@ -28,7 +28,34 @@ resource "aws_security_group" "api_sg" { } } -# 2. RDS용 보안 그룹 (API Server만 믿음) +# 2. DB EC2용 보안 그룹 (API Server만 믿음) +resource "aws_security_group" "db_ec2_sg" { + count = var.enable_db_ec2 ? 1 : 0 + name = "sc-${var.env_name}-db-ec2-sg" + description = "Security Group for DB EC2" + vpc_id = var.vpc_id + + ingress { + description = "MySQL from API server" + from_port = 3306 + to_port = 3306 + protocol = "tcp" + security_groups = [aws_security_group.api_sg.id] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "solid-connection-${var.env_name}-db-ec2-sg" + } +} + +# 3. RDS용 보안 그룹 (API Server만 믿음) resource "aws_security_group" "db_sg" { count = var.enable_rds ? 1 : 0 name = "sc-${var.env_name}-db-sg" diff --git a/modules/app_stack/variables.tf b/modules/app_stack/variables.tf index aa41e15..edee401 100644 --- a/modules/app_stack/variables.tf +++ b/modules/app_stack/variables.tf @@ -12,6 +12,24 @@ variable "enable_rds" { default = true } +variable "enable_db_ec2" { + description = "DB EC2 사용 여부" + type = bool + default = false +} + +variable "db_instance_type" { + description = "DB EC2 인스턴스 타입" + type = string + default = null +} + +variable "db_ami_id" { + description = "DB EC2에 사용할 커스텀 AMI ID" + type = string + default = null +} + variable "ec2_iam_instance_profile" { description = "EC2에 연결할 IAM Instance Profile 이름" type = string From b81398e8066c2b8264d9275c18de2dab7a56dba5 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Tue, 23 Jun 2026 00:34:04 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20DB=20EC2=20MySQL=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=B4=88=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app_stack/scripts/mysql_setup.sh.tftpl | 53 +++++++++++++++++++ modules/app_stack/templates/mysql_tuning.cnf | 6 +++ 2 files changed, 59 insertions(+) create mode 100644 modules/app_stack/scripts/mysql_setup.sh.tftpl create mode 100644 modules/app_stack/templates/mysql_tuning.cnf diff --git a/modules/app_stack/scripts/mysql_setup.sh.tftpl b/modules/app_stack/scripts/mysql_setup.sh.tftpl new file mode 100644 index 0000000..c47065f --- /dev/null +++ b/modules/app_stack/scripts/mysql_setup.sh.tftpl @@ -0,0 +1,53 @@ +#!/bin/bash +set -euo pipefail + +DB_ROOT_USER="$(printf '%s' '${db_root_username_b64}' | base64 -d)" +DB_ROOT_PASS="$(printf '%s' '${db_root_password_b64}' | base64 -d)" + +mysql_escape() { + local value="$1" + value="$${value//\\/\\\\}" + value="$${value//\'/\\\'}" + printf '%s' "$value" +} + +DB_ROOT_USER_SQL="$(mysql_escape "$DB_ROOT_USER")" +DB_ROOT_PASS_SQL="$(mysql_escape "$DB_ROOT_PASS")" + +command -v docker >/dev/null +systemctl enable --now docker +docker image inspect mysql:8.4 >/dev/null + +mkdir -p /var/lib/mysql +chown -R 999:999 /var/lib/mysql +chmod 750 /var/lib/mysql + +mkdir -p /etc/mysql/conf.d +cat > /etc/mysql/conf.d/tuning.cnf <<'CNFEOF' +${mysql_config_content} +CNFEOF +chmod 644 /etc/mysql/conf.d/tuning.cnf + +docker rm -f mysql-server 2>/dev/null || true + +docker run -d \ + --name mysql-server \ + --restart always \ + -p 3306:3306 \ + -v /var/lib/mysql:/var/lib/mysql \ + -v /etc/mysql/conf.d:/etc/mysql/conf.d \ + -e MYSQL_ROOT_PASSWORD="$DB_ROOT_PASS" \ + mysql:8.4 + +for i in $(seq 1 30); do + if docker exec mysql-server mysqladmin ping -uroot -p"$DB_ROOT_PASS" 2>/dev/null; then + break + fi + sleep 2 +done + +docker exec -i mysql-server mysql -uroot -p"$DB_ROOT_PASS" < Date: Tue, 23 Jun 2026 00:34:25 +0900 Subject: [PATCH 3/7] =?UTF-8?q?chore:=20Prod=20DB=20EC2=20tfvars=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/secrets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/secrets b/config/secrets index ad87e45..331c8c3 160000 --- a/config/secrets +++ b/config/secrets @@ -1 +1 @@ -Subproject commit ad87e450f7a7e9b718ed9c1cc32908884011f80c +Subproject commit 331c8c34e1ef79bb05cc2edd262ed2a3935c8a31 From d64bc19f5ec8d357980323bfd173aad007033953 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Tue, 23 Jun 2026 00:56:35 +0900 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20SSM=20=ED=8A=9C=EB=8B=9D=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0(Is?= =?UTF-8?q?sue=2051=EC=97=90=EC=84=9C=20=EB=8B=A4=EB=A3=B0=20=EA=B3=84?= =?UTF-8?q?=ED=9A=8D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/secrets | 2 +- modules/app_stack/db_ec2.tf | 61 ------------------------------------- 2 files changed, 1 insertion(+), 62 deletions(-) diff --git a/config/secrets b/config/secrets index 331c8c3..921312e 160000 --- a/config/secrets +++ b/config/secrets @@ -1 +1 @@ -Subproject commit 331c8c34e1ef79bb05cc2edd262ed2a3935c8a31 +Subproject commit 921312e61a42342d5fe387a2df8ba7c25af67e76 diff --git a/modules/app_stack/db_ec2.tf b/modules/app_stack/db_ec2.tf index ac12290..5e637d6 100644 --- a/modules/app_stack/db_ec2.tf +++ b/modules/app_stack/db_ec2.tf @@ -14,22 +14,6 @@ data "cloudinit_config" "db_init" { } } -locals { - mysql_tuning_config_b64 = base64encode(file("${path.module}/templates/mysql_tuning.cnf")) - - mysql_tuning_ssm_params = jsonencode({ - commands = [ - "cloud-init status --wait > /dev/null", - "sudo mkdir -p /etc/mysql/conf.d", - "echo ${local.mysql_tuning_config_b64} | base64 -d | sudo tee /etc/mysql/conf.d/tuning.cnf > /dev/null", - "sudo chmod 644 /etc/mysql/conf.d/tuning.cnf", - "sudo docker restart mysql-server", - "for i in $(seq 1 30); do if sudo docker exec mysql-server mysqladmin ping --silent >/dev/null 2>&1; then exit 0; fi; sleep 2; done; sudo docker logs --tail 100 mysql-server >&2; exit 1", - ] - executionTimeout = ["600"] - }) -} - resource "aws_instance" "db_server" { count = var.enable_db_ec2 ? 1 : 0 @@ -71,48 +55,3 @@ resource "aws_instance" "db_server" { ] } } - -resource "null_resource" "update_mysql_tuning" { - count = var.enable_db_ec2 ? 1 : 0 - depends_on = [aws_instance.db_server] - - triggers = { - config_hash = sha256(file("${path.module}/templates/mysql_tuning.cnf")) - instance_id = aws_instance.db_server[count.index].id - } - - provisioner "local-exec" { - interpreter = ["bash", "-c"] - command = <<-EOT - set -euo pipefail - INSTANCE_ID='${aws_instance.db_server[count.index].id}' - COMMAND_ID=$(aws ssm send-command \ - --instance-ids "$INSTANCE_ID" \ - --document-name "AWS-RunShellScript" \ - --parameters '${local.mysql_tuning_ssm_params}' \ - --output text \ - --query "Command.CommandId") - ATTEMPTS=0 - while [ "$ATTEMPTS" -lt 60 ]; do - STATUS=$(aws ssm get-command-invocation \ - --command-id "$COMMAND_ID" \ - --instance-id "$INSTANCE_ID" \ - --query "Status" --output text 2>/dev/null || echo "Pending") - case "$STATUS" in - Success) exit 0 ;; - Failed|Cancelled|TimedOut|Undeliverable) - echo "SSM command $STATUS" >&2 - aws ssm get-command-invocation \ - --command-id "$COMMAND_ID" \ - --instance-id "$INSTANCE_ID" \ - --query "StandardErrorContent" --output text >&2 - exit 1 ;; - esac - ATTEMPTS=$((ATTEMPTS + 1)) - sleep 10 - done - echo "SSM command timed out after 600s" >&2 - exit 1 - EOT - } -} From 42b40cd8e88f5d71af8a3feccf56671377bad8b4 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Wed, 24 Jun 2026 12:58:07 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20Prod=20DB=20EC2=EB=A5=BC=20Private?= =?UTF-8?q?=20Subnet=EC=97=90=20=EB=B0=B0=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/secrets | 2 +- environment/prod/main.tf | 1 + environment/prod/variables.tf | 5 +++++ modules/app_stack/db_ec2.tf | 1 + modules/app_stack/variables.tf | 6 ++++++ 5 files changed, 14 insertions(+), 1 deletion(-) diff --git a/config/secrets b/config/secrets index 921312e..490b934 160000 --- a/config/secrets +++ b/config/secrets @@ -1 +1 @@ -Subproject commit 921312e61a42342d5fe387a2df8ba7c25af67e76 +Subproject commit 490b9349618bf38aa9d3cfe7d622908e53a851fe diff --git a/environment/prod/main.tf b/environment/prod/main.tf index 0accfb4..9a82f2b 100644 --- a/environment/prod/main.tf +++ b/environment/prod/main.tf @@ -25,6 +25,7 @@ module "prod_stack" { enable_db_ec2 = true db_instance_type = var.db_ec2_instance_type db_ami_id = var.db_ec2_ami_id + db_subnet_id = var.db_ec2_subnet_id # 보안 그룹 규칙 api_ingress_rules = var.api_ingress_rules diff --git a/environment/prod/variables.tf b/environment/prod/variables.tf index 83a608f..ce43938 100644 --- a/environment/prod/variables.tf +++ b/environment/prod/variables.tf @@ -28,6 +28,11 @@ variable "db_ec2_ami_id" { type = string } +variable "db_ec2_subnet_id" { + description = "DB EC2를 배치할 Private Subnet ID" + type = string +} + variable "api_ingress_rules" { description = "List of ingress rules for API Server" type = list(object({ diff --git a/modules/app_stack/db_ec2.tf b/modules/app_stack/db_ec2.tf index 5e637d6..460094a 100644 --- a/modules/app_stack/db_ec2.tf +++ b/modules/app_stack/db_ec2.tf @@ -19,6 +19,7 @@ resource "aws_instance" "db_server" { ami = var.db_ami_id instance_type = var.db_instance_type + subnet_id = var.db_subnet_id vpc_security_group_ids = [aws_security_group.db_ec2_sg[count.index].id] associate_public_ip_address = false diff --git a/modules/app_stack/variables.tf b/modules/app_stack/variables.tf index edee401..f4ac48d 100644 --- a/modules/app_stack/variables.tf +++ b/modules/app_stack/variables.tf @@ -30,6 +30,12 @@ variable "db_ami_id" { default = null } +variable "db_subnet_id" { + description = "DB EC2를 배치할 Private Subnet ID" + type = string + default = null +} + variable "ec2_iam_instance_profile" { description = "EC2에 연결할 IAM Instance Profile 이름" type = string From 9f4fd00f4146d6bb1d4d2d847c018ce347c8d3a4 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Wed, 24 Jun 2026 13:41:40 +0900 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20mysql=20container=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=8B=9C=20=ED=97=AC=EC=8A=A4=EC=B2=B4=ED=81=AC=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EB=A1=9C=EA=B7=B8=20=EC=B6=9C=EB=A0=A5=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/app_stack/scripts/mysql_setup.sh.tftpl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/app_stack/scripts/mysql_setup.sh.tftpl b/modules/app_stack/scripts/mysql_setup.sh.tftpl index c47065f..ff208ac 100644 --- a/modules/app_stack/scripts/mysql_setup.sh.tftpl +++ b/modules/app_stack/scripts/mysql_setup.sh.tftpl @@ -39,13 +39,21 @@ docker run -d \ -e MYSQL_ROOT_PASSWORD="$DB_ROOT_PASS" \ mysql:8.4 +MYSQL_READY=false for i in $(seq 1 30); do if docker exec mysql-server mysqladmin ping -uroot -p"$DB_ROOT_PASS" 2>/dev/null; then + MYSQL_READY=true break fi sleep 2 done +if [ "$MYSQL_READY" != "true" ]; then + echo "MySQL container did not become ready within 60 seconds." >&2 + docker logs --tail 100 mysql-server >&2 || true + exit 1 +fi + docker exec -i mysql-server mysql -uroot -p"$DB_ROOT_PASS" < Date: Wed, 24 Jun 2026 14:44:25 +0900 Subject: [PATCH 7/7] =?UTF-8?q?chore:=20DB=20EC2=20=EB=A3=A8=ED=8A=B8=20?= =?UTF-8?q?=EB=B3=BC=EB=A5=A8=20=ED=81=AC=EA=B8=B0=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/app_stack/db_ec2.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/app_stack/db_ec2.tf b/modules/app_stack/db_ec2.tf index 460094a..a3b26b1 100644 --- a/modules/app_stack/db_ec2.tf +++ b/modules/app_stack/db_ec2.tf @@ -35,7 +35,7 @@ resource "aws_instance" "db_server" { } root_block_device { - volume_size = 20 + volume_size = 8 volume_type = "gp3" encrypted = true delete_on_termination = true