diff --git a/config/secrets b/config/secrets index 75b92d2..c1eaa90 160000 --- a/config/secrets +++ b/config/secrets @@ -1 +1 @@ -Subproject commit 75b92d219d9c4ae2533dc6220f886f289a1a1c3b +Subproject commit c1eaa90de823b64108732407523b1c78a9873c40 diff --git a/environment/prod/main.tf b/environment/prod/main.tf index 9a82f2b..be2d166 100644 --- a/environment/prod/main.tf +++ b/environment/prod/main.tf @@ -22,10 +22,11 @@ module "prod_stack" { 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 - db_subnet_id = var.db_ec2_subnet_id + 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 + db_data_volume_size = var.db_data_volume_size # 보안 그룹 규칙 api_ingress_rules = var.api_ingress_rules diff --git a/environment/prod/variables.tf b/environment/prod/variables.tf index ce43938..997232e 100644 --- a/environment/prod/variables.tf +++ b/environment/prod/variables.tf @@ -33,6 +33,11 @@ variable "db_ec2_subnet_id" { type = string } +variable "db_data_volume_size" { + description = "DB EC2 MySQL data volume 크기 (GiB)" + type = number +} + 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 a3b26b1..deed39f 100644 --- a/modules/app_stack/db_ec2.tf +++ b/modules/app_stack/db_ec2.tf @@ -1,3 +1,26 @@ +data "aws_subnet" "db_ec2" { + count = var.enable_db_ec2 ? 1 : 0 + + id = var.db_subnet_id +} + +resource "aws_ebs_volume" "db_data" { + count = var.enable_db_ec2 ? 1 : 0 + + availability_zone = data.aws_subnet.db_ec2[count.index].availability_zone + size = var.db_data_volume_size + type = "gp3" + encrypted = true + + tags = { + Name = "solid-connection-db-mysql-data-${var.env_name}" + } + + lifecycle { + prevent_destroy = true + } +} + data "cloudinit_config" "db_init" { count = var.enable_db_ec2 ? 1 : 0 gzip = true @@ -8,6 +31,7 @@ data "cloudinit_config" "db_init" { 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) + db_data_volume_id = aws_ebs_volume.db_data[count.index].id mysql_config_content = file("${path.module}/templates/mysql_tuning.cnf") }) filename = "mysql_setup.sh" @@ -45,14 +69,20 @@ resource "aws_instance" "db_server" { Name = "solid-connection-db-mysql-${var.env_name}" } - user_data_replace_on_change = false + user_data_replace_on_change = true lifecycle { ignore_changes = [ - user_data, - user_data_base64, - user_data_replace_on_change, key_name, ] } } + +resource "aws_volume_attachment" "db_data" { + count = var.enable_db_ec2 ? 1 : 0 + + device_name = "/dev/sdf" + volume_id = aws_ebs_volume.db_data[count.index].id + instance_id = aws_instance.db_server[count.index].id + stop_instance_before_detaching = true +} diff --git a/modules/app_stack/output.tf b/modules/app_stack/output.tf index 523c77a..e9e15cf 100644 --- a/modules/app_stack/output.tf +++ b/modules/app_stack/output.tf @@ -7,3 +7,8 @@ output "db_server_instance_id" { description = "DB EC2 서버 인스턴스 ID" value = try(aws_instance.db_server[0].id, null) } + +output "db_server_data_volume_id" { + description = "DB EC2 MySQL data volume ID" + value = try(aws_ebs_volume.db_data[0].id, null) +} diff --git a/modules/app_stack/scripts/mysql_setup.sh.tftpl b/modules/app_stack/scripts/mysql_setup.sh.tftpl index 268a323..1904877 100644 --- a/modules/app_stack/scripts/mysql_setup.sh.tftpl +++ b/modules/app_stack/scripts/mysql_setup.sh.tftpl @@ -15,12 +15,61 @@ 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 +command -v blkid >/dev/null +command -v mkfs.ext4 >/dev/null +command -v mountpoint >/dev/null + +systemctl enable docker + +DATA_VOLUME_ID="${db_data_volume_id}" +DATA_VOLUME_SERIAL="$(printf '%s' "$DATA_VOLUME_ID" | tr -d '-')" +DATA_VOLUME_DEVICE="/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_$DATA_VOLUME_SERIAL" +DATA_VOLUME_MOUNT="/mnt/mysql-data" +MYSQL_DATA_DIR="$DATA_VOLUME_MOUNT/mysql" + +for i in $(seq 1 120); do + if [ -e "$DATA_VOLUME_DEVICE" ]; then + break + fi + sleep 2 +done + +if [ ! -e "$DATA_VOLUME_DEVICE" ]; then + echo "Data volume $DATA_VOLUME_ID was not attached within 240 seconds." >&2 + ls -la /dev/disk/by-id >&2 || true + exit 1 +fi + +if ! blkid "$DATA_VOLUME_DEVICE" >/dev/null 2>&1; then + mkfs.ext4 -F "$DATA_VOLUME_DEVICE" +fi + +mkdir -p "$DATA_VOLUME_MOUNT" +DATA_VOLUME_UUID="$(blkid -s UUID -o value "$DATA_VOLUME_DEVICE")" + +awk -v mount="$DATA_VOLUME_MOUNT" '$2 != mount { print }' /etc/fstab > /etc/fstab.tmp +printf 'UUID=%s %s ext4 defaults,nofail 0 2\n' "$DATA_VOLUME_UUID" "$DATA_VOLUME_MOUNT" >> /etc/fstab.tmp +mv /etc/fstab.tmp /etc/fstab + +mountpoint -q "$DATA_VOLUME_MOUNT" || mount "$DATA_VOLUME_MOUNT" +mountpoint -q "$DATA_VOLUME_MOUNT" || { + echo "Failed to mount data volume $DATA_VOLUME_ID at $DATA_VOLUME_MOUNT." >&2 + exit 1 +} + +mkdir -p /etc/systemd/system/docker.service.d +cat > /etc/systemd/system/docker.service.d/10-require-mysql-data.conf </dev/null -mkdir -p /var/lib/mysql -chown -R 999:999 /var/lib/mysql -chmod 750 /var/lib/mysql +mkdir -p "$MYSQL_DATA_DIR" +chown -R 999:999 "$MYSQL_DATA_DIR" +chmod 750 "$MYSQL_DATA_DIR" mkdir -p /etc/mysql/conf.d cat > /etc/mysql/conf.d/tuning.cnf <<'CNFEOF' @@ -34,7 +83,7 @@ docker run -d \ --name mysql-server \ --restart always \ -p 3306:3306 \ - -v /var/lib/mysql:/var/lib/mysql \ + -v "$MYSQL_DATA_DIR:/var/lib/mysql" \ -v /etc/mysql/conf.d:/etc/mysql/conf.d \ -e MYSQL_ROOT_PASSWORD="$DB_ROOT_PASS" \ mysql:8.4.8 diff --git a/modules/app_stack/variables.tf b/modules/app_stack/variables.tf index f4ac48d..b143250 100644 --- a/modules/app_stack/variables.tf +++ b/modules/app_stack/variables.tf @@ -36,6 +36,22 @@ variable "db_subnet_id" { default = null } +variable "db_data_volume_size" { + description = "DB EC2 MySQL data volume 크기 (GiB)" + type = number + default = null + + validation { + condition = !var.enable_db_ec2 || var.db_data_volume_size != null + error_message = "enable_db_ec2가 true이면 db_data_volume_size를 지정해야 합니다." + } + + validation { + condition = var.db_data_volume_size == null ? true : var.db_data_volume_size >= 1 + error_message = "db_data_volume_size는 1GiB 이상이어야 합니다." + } +} + variable "ec2_iam_instance_profile" { description = "EC2에 연결할 IAM Instance Profile 이름" type = string