diff --git a/modules/app_stack/db_ec2.tf b/modules/app_stack/db_ec2.tf index fbfccb4..e18eecb 100644 --- a/modules/app_stack/db_ec2.tf +++ b/modules/app_stack/db_ec2.tf @@ -4,6 +4,8 @@ data "aws_subnet" "db_ec2" { id = var.db_subnet_id } +# 운영 데이터 볼륨은 app_stack 에 남겨 기존 state 주소와 prevent_destroy 보호를 유지합니다. +# 재사용 가능한 db_ec2 모듈은 전달받은 볼륨을 DB EC2에 연결하는 역할만 담당합니다. resource "aws_ebs_volume" "db_data" { count = var.enable_db_ec2 ? 1 : 0 @@ -21,78 +23,20 @@ resource "aws_ebs_volume" "db_data" { } } -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) - 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" - } -} - -resource "aws_instance" "db_server" { - count = var.enable_db_ec2 ? 1 : 0 - - ami = var.db_ami_id - instance_type = var.db_instance_type - subnet_id = var.db_subnet_id - # 인스턴스를 교체해도 Parameter Store 의 datasource 주소와 API 서버 설정을 그대로 쓰도록 주소를 고정합니다. - private_ip = var.db_private_ip - - vpc_security_group_ids = [ - aws_security_group.db_ec2_sg[count.index].id, - aws_security_group.db_ec2_alarm_client_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 = 8 - 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 { - # AMI 갱신이 운영 중인 DB EC2를 교체하지 않도록 무시하고, 인스턴스가 재생성되는 시점에 새 AMI를 적용합니다. - ignore_changes = [ - ami, - key_name, - user_data, - user_data_base64, - user_data_replace_on_change, - ] - } -} - -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 +module "db_ec2" { + source = "./modules/db_ec2" + + enabled = var.enable_db_ec2 + env_name = var.env_name + db_instance_type = var.db_instance_type + db_ami_id = var.db_ami_id + db_subnet_id = var.db_subnet_id + db_private_ip = var.db_private_ip + data_volume_id = try(aws_ebs_volume.db_data[0].id, null) + db_security_group_id = try(aws_security_group.db_ec2_sg[0].id, null) + alarm_client_security_group_id = try(aws_security_group.db_ec2_alarm_client_sg[0].id, null) + ec2_iam_instance_profile = var.ec2_iam_instance_profile + key_name = var.key_name + db_username = var.db_username + db_password = var.db_password } diff --git a/modules/app_stack/db_users.tf b/modules/app_stack/db_users.tf index 1eb6033..1c51dfd 100644 --- a/modules/app_stack/db_users.tf +++ b/modules/app_stack/db_users.tf @@ -11,7 +11,7 @@ resource "mysql_user" "users" { depends_on = [ aws_db_instance.default, - aws_instance.db_server, + module.db_ec2, ] } diff --git a/modules/app_stack/ec2.tf b/modules/app_stack/ec2.tf index d2f126b..34e784c 100644 --- a/modules/app_stack/ec2.tf +++ b/modules/app_stack/ec2.tf @@ -1,289 +1,20 @@ -data "aws_instance" "monitoring_server" { - filter { - name = "tag:Name" - values = ["solid-connection-monitoring"] - } - - filter { - name = "instance-state-name" - values = ["running"] - } -} - -# CloudInit을 이용한 User Data 스크립트 구성 -data "cloudinit_config" "app_init" { - gzip = true - base64_encode = true - - # [Part 1] Docker 설치 스크립트 - part { - content_type = "text/x-shellscript" - content = file("${path.module}/../common/scripts/docker_setup.sh") - filename = "1_docker_install.sh" - } - -} - -# API Server (EC2) -resource "aws_instance" "api_server" { - ami = var.ami_id - instance_type = var.instance_type - - vpc_security_group_ids = [aws_security_group.api_sg.id] - - key_name = var.key_name - associate_public_ip_address = true - iam_instance_profile = var.ec2_iam_instance_profile - - user_data_base64 = data.cloudinit_config.app_init.rendered - - tags = { - Name = "solid-connection-server-${var.env_name}" - } - - user_data_replace_on_change = false - - lifecycle { - ignore_changes = [ - user_data, - user_data_base64, - user_data_replace_on_change, - - ami, - key_name - ] - } -} - -locals { - nginx_install_script_b64 = base64encode(templatefile("${path.module}/scripts/nginx_install.sh.tftpl", { - domain_name = var.domain_name - email = var.cert_email - conf_file_name = var.nginx_conf_name - })) - - nginx_conf_script_b64 = base64encode(templatefile("${path.module}/scripts/nginx_conf.sh.tftpl", { - domain_name = var.domain_name - conf_file_name = var.nginx_conf_name - })) - - alloy_config = templatefile("${path.module}/../../config/side-infra/config.alloy.tftpl", { - loki_ip = data.aws_instance.monitoring_server.private_ip - }) - - side_infra_script_b64 = base64encode(templatefile("${path.module}/scripts/side_infra_setup.sh.tftpl", { - work_dir = var.work_dir - alloy_env_name = var.alloy_env_name - alloy_config_content = local.alloy_config - redis_version = var.redis_version - redis_exporter_version = var.redis_exporter_version - alloy_version = var.alloy_version - })) - - nginx_install_ssm_params = jsonencode({ - commands = ["cloud-init status --wait > /dev/null", "echo ${local.nginx_install_script_b64} | base64 -d | sudo bash"] - executionTimeout = ["3600"] - }) - - nginx_conf_ssm_params = jsonencode({ - commands = ["cloud-init status --wait > /dev/null", "echo ${local.nginx_conf_script_b64} | base64 -d | sudo bash"] - executionTimeout = ["3600"] - }) - - side_infra_ssm_params = jsonencode({ - commands = ["cloud-init status --wait > /dev/null", "echo ${local.side_infra_script_b64} | base64 -d | sudo bash"] - executionTimeout = ["3600"] - }) -} - -# [리소스 1] Nginx 설치 및 인증서 확보 -# 설치가 이미 끝나 있으면 스크립트가 즉시 종료하므로 평상시에는 사실상 실행되지 않습니다. -resource "null_resource" "install_nginx" { - depends_on = [aws_instance.api_server] - - triggers = { - script_hash = sha256(templatefile("${path.module}/scripts/nginx_install.sh.tftpl", { - domain_name = var.domain_name - email = var.cert_email - conf_file_name = var.nginx_conf_name - })) - } - - provisioner "local-exec" { - interpreter = ["bash", "-c"] - command = <<-EOT - set -euo pipefail - INSTANCE_ID='${aws_instance.api_server.id}' - - # 인스턴스가 교체된 직후에는 SSM 에이전트가 아직 등록되지 않아 - # send-command 가 InvalidInstanceId 로 즉시 실패합니다. 등록될 때까지 기다립니다. - PING_STATUS="" - SSM_ATTEMPTS=0 - while [ "$SSM_ATTEMPTS" -lt 60 ]; do - PING_STATUS=$(aws ssm describe-instance-information \ - --filters "Key=InstanceIds,Values=$INSTANCE_ID" \ - --query "InstanceInformationList[0].PingStatus" \ - --output text 2>/dev/null || echo "None") - if [ "$PING_STATUS" = "Online" ]; then - break - fi - SSM_ATTEMPTS=$((SSM_ATTEMPTS + 1)) - sleep 10 - done - if [ "$PING_STATUS" != "Online" ]; then - echo "SSM agent not registered within 600s (last status: $PING_STATUS)" >&2 - exit 1 - fi - - COMMAND_ID=$(aws ssm send-command \ - --instance-ids "$INSTANCE_ID" \ - --document-name "AWS-RunShellScript" \ - --parameters '${local.nginx_install_ssm_params}' \ - --output text \ - --query "Command.CommandId") - ATTEMPTS=0 - while [ "$ATTEMPTS" -lt 360 ]; 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 3600s" >&2 - exit 1 - EOT - } -} - -# [리소스 2] Nginx 설정 변경 감지 및 실행 -# 설정 템플릿이 바뀌면 이 리소스만 재생성되어, 설치 작업 없이 conf 재작성과 reload 만 수행합니다. -resource "null_resource" "update_nginx_conf" { - depends_on = [null_resource.install_nginx] - - triggers = { - script_hash = sha256(templatefile("${path.module}/scripts/nginx_conf.sh.tftpl", { - domain_name = var.domain_name - conf_file_name = var.nginx_conf_name - })) - } - - provisioner "local-exec" { - interpreter = ["bash", "-c"] - command = <<-EOT - set -euo pipefail - INSTANCE_ID='${aws_instance.api_server.id}' - - # 인스턴스가 교체된 직후에는 SSM 에이전트가 아직 등록되지 않아 - # send-command 가 InvalidInstanceId 로 즉시 실패합니다. 등록될 때까지 기다립니다. - PING_STATUS="" - SSM_ATTEMPTS=0 - while [ "$SSM_ATTEMPTS" -lt 60 ]; do - PING_STATUS=$(aws ssm describe-instance-information \ - --filters "Key=InstanceIds,Values=$INSTANCE_ID" \ - --query "InstanceInformationList[0].PingStatus" \ - --output text 2>/dev/null || echo "None") - if [ "$PING_STATUS" = "Online" ]; then - break - fi - SSM_ATTEMPTS=$((SSM_ATTEMPTS + 1)) - sleep 10 - done - if [ "$PING_STATUS" != "Online" ]; then - echo "SSM agent not registered within 600s (last status: $PING_STATUS)" >&2 - exit 1 - fi - - COMMAND_ID=$(aws ssm send-command \ - --instance-ids "$INSTANCE_ID" \ - --document-name "AWS-RunShellScript" \ - --parameters '${local.nginx_conf_ssm_params}' \ - --output text \ - --query "Command.CommandId") - ATTEMPTS=0 - while [ "$ATTEMPTS" -lt 360 ]; 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 3600s" >&2 - exit 1 - EOT - } -} - -# [리소스 3] Side Infra 설정 변경 감지 및 실행 -resource "null_resource" "update_side_infra" { - depends_on = [aws_instance.api_server] - - triggers = { - script_hash = sha256(templatefile("${path.module}/scripts/side_infra_setup.sh.tftpl", { - work_dir = var.work_dir - alloy_env_name = var.alloy_env_name - alloy_config_content = templatefile("${path.module}/../../config/side-infra/config.alloy.tftpl", { - loki_ip = data.aws_instance.monitoring_server.private_ip - }) - redis_version = var.redis_version - redis_exporter_version = var.redis_exporter_version - alloy_version = var.alloy_version - })) - } - - provisioner "local-exec" { - interpreter = ["bash", "-c"] - command = <<-EOT - set -euo pipefail - INSTANCE_ID='${aws_instance.api_server.id}' - COMMAND_ID=$(aws ssm send-command \ - --instance-ids "$INSTANCE_ID" \ - --document-name "AWS-RunShellScript" \ - --parameters '${local.side_infra_ssm_params}' \ - --output text \ - --query "Command.CommandId") - ATTEMPTS=0 - while [ "$ATTEMPTS" -lt 360 ]; 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 3600s" >&2 - exit 1 - EOT - } +module "app" { + source = "./modules/app" + + env_name = var.env_name + ami_id = var.ami_id + instance_type = var.instance_type + api_security_group_id = aws_security_group.api_sg.id + ec2_iam_instance_profile = var.ec2_iam_instance_profile + key_name = var.key_name + + domain_name = var.domain_name + cert_email = var.cert_email + nginx_conf_name = var.nginx_conf_name + + work_dir = var.work_dir + alloy_env_name = var.alloy_env_name + redis_version = var.redis_version + redis_exporter_version = var.redis_exporter_version + alloy_version = var.alloy_version } diff --git a/modules/app_stack/modules/app/main.tf b/modules/app_stack/modules/app/main.tf new file mode 100644 index 0000000..b010fb2 --- /dev/null +++ b/modules/app_stack/modules/app/main.tf @@ -0,0 +1,289 @@ +data "aws_instance" "monitoring_server" { + filter { + name = "tag:Name" + values = ["solid-connection-monitoring"] + } + + filter { + name = "instance-state-name" + values = ["running"] + } +} + +# CloudInit을 이용한 User Data 스크립트 구성 +data "cloudinit_config" "app_init" { + gzip = true + base64_encode = true + + # [Part 1] Docker 설치 스크립트 + part { + content_type = "text/x-shellscript" + content = file("${path.module}/../../../common/scripts/docker_setup.sh") + filename = "1_docker_install.sh" + } + +} + +# API Server (EC2) +resource "aws_instance" "api_server" { + ami = var.ami_id + instance_type = var.instance_type + + vpc_security_group_ids = [var.api_security_group_id] + + key_name = var.key_name + associate_public_ip_address = true + iam_instance_profile = var.ec2_iam_instance_profile + + user_data_base64 = data.cloudinit_config.app_init.rendered + + tags = { + Name = "solid-connection-server-${var.env_name}" + } + + user_data_replace_on_change = false + + lifecycle { + ignore_changes = [ + user_data, + user_data_base64, + user_data_replace_on_change, + + ami, + key_name + ] + } +} + +locals { + nginx_install_script_b64 = base64encode(templatefile("${path.module}/../../scripts/nginx_install.sh.tftpl", { + domain_name = var.domain_name + email = var.cert_email + conf_file_name = var.nginx_conf_name + })) + + nginx_conf_script_b64 = base64encode(templatefile("${path.module}/../../scripts/nginx_conf.sh.tftpl", { + domain_name = var.domain_name + conf_file_name = var.nginx_conf_name + })) + + alloy_config = templatefile("${path.module}/../../../../config/side-infra/config.alloy.tftpl", { + loki_ip = data.aws_instance.monitoring_server.private_ip + }) + + side_infra_script_b64 = base64encode(templatefile("${path.module}/../../scripts/side_infra_setup.sh.tftpl", { + work_dir = var.work_dir + alloy_env_name = var.alloy_env_name + alloy_config_content = local.alloy_config + redis_version = var.redis_version + redis_exporter_version = var.redis_exporter_version + alloy_version = var.alloy_version + })) + + nginx_install_ssm_params = jsonencode({ + commands = ["cloud-init status --wait > /dev/null", "echo ${local.nginx_install_script_b64} | base64 -d | sudo bash"] + executionTimeout = ["3600"] + }) + + nginx_conf_ssm_params = jsonencode({ + commands = ["cloud-init status --wait > /dev/null", "echo ${local.nginx_conf_script_b64} | base64 -d | sudo bash"] + executionTimeout = ["3600"] + }) + + side_infra_ssm_params = jsonencode({ + commands = ["cloud-init status --wait > /dev/null", "echo ${local.side_infra_script_b64} | base64 -d | sudo bash"] + executionTimeout = ["3600"] + }) +} + +# [리소스 1] Nginx 설치 및 인증서 확보 +# 설치가 이미 끝나 있으면 스크립트가 즉시 종료하므로 평상시에는 사실상 실행되지 않습니다. +resource "null_resource" "install_nginx" { + depends_on = [aws_instance.api_server] + + triggers = { + script_hash = sha256(templatefile("${path.module}/../../scripts/nginx_install.sh.tftpl", { + domain_name = var.domain_name + email = var.cert_email + conf_file_name = var.nginx_conf_name + })) + } + + provisioner "local-exec" { + interpreter = ["bash", "-c"] + command = <<-EOT + set -euo pipefail + INSTANCE_ID='${aws_instance.api_server.id}' + + # 인스턴스가 교체된 직후에는 SSM 에이전트가 아직 등록되지 않아 + # send-command 가 InvalidInstanceId 로 즉시 실패합니다. 등록될 때까지 기다립니다. + PING_STATUS="" + SSM_ATTEMPTS=0 + while [ "$SSM_ATTEMPTS" -lt 60 ]; do + PING_STATUS=$(aws ssm describe-instance-information \ + --filters "Key=InstanceIds,Values=$INSTANCE_ID" \ + --query "InstanceInformationList[0].PingStatus" \ + --output text 2>/dev/null || echo "None") + if [ "$PING_STATUS" = "Online" ]; then + break + fi + SSM_ATTEMPTS=$((SSM_ATTEMPTS + 1)) + sleep 10 + done + if [ "$PING_STATUS" != "Online" ]; then + echo "SSM agent not registered within 600s (last status: $PING_STATUS)" >&2 + exit 1 + fi + + COMMAND_ID=$(aws ssm send-command \ + --instance-ids "$INSTANCE_ID" \ + --document-name "AWS-RunShellScript" \ + --parameters '${local.nginx_install_ssm_params}' \ + --output text \ + --query "Command.CommandId") + ATTEMPTS=0 + while [ "$ATTEMPTS" -lt 360 ]; 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 3600s" >&2 + exit 1 + EOT + } +} + +# [리소스 2] Nginx 설정 변경 감지 및 실행 +# 설정 템플릿이 바뀌면 이 리소스만 재생성되어, 설치 작업 없이 conf 재작성과 reload 만 수행합니다. +resource "null_resource" "update_nginx_conf" { + depends_on = [null_resource.install_nginx] + + triggers = { + script_hash = sha256(templatefile("${path.module}/../../scripts/nginx_conf.sh.tftpl", { + domain_name = var.domain_name + conf_file_name = var.nginx_conf_name + })) + } + + provisioner "local-exec" { + interpreter = ["bash", "-c"] + command = <<-EOT + set -euo pipefail + INSTANCE_ID='${aws_instance.api_server.id}' + + # 인스턴스가 교체된 직후에는 SSM 에이전트가 아직 등록되지 않아 + # send-command 가 InvalidInstanceId 로 즉시 실패합니다. 등록될 때까지 기다립니다. + PING_STATUS="" + SSM_ATTEMPTS=0 + while [ "$SSM_ATTEMPTS" -lt 60 ]; do + PING_STATUS=$(aws ssm describe-instance-information \ + --filters "Key=InstanceIds,Values=$INSTANCE_ID" \ + --query "InstanceInformationList[0].PingStatus" \ + --output text 2>/dev/null || echo "None") + if [ "$PING_STATUS" = "Online" ]; then + break + fi + SSM_ATTEMPTS=$((SSM_ATTEMPTS + 1)) + sleep 10 + done + if [ "$PING_STATUS" != "Online" ]; then + echo "SSM agent not registered within 600s (last status: $PING_STATUS)" >&2 + exit 1 + fi + + COMMAND_ID=$(aws ssm send-command \ + --instance-ids "$INSTANCE_ID" \ + --document-name "AWS-RunShellScript" \ + --parameters '${local.nginx_conf_ssm_params}' \ + --output text \ + --query "Command.CommandId") + ATTEMPTS=0 + while [ "$ATTEMPTS" -lt 360 ]; 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 3600s" >&2 + exit 1 + EOT + } +} + +# [리소스 3] Side Infra 설정 변경 감지 및 실행 +resource "null_resource" "update_side_infra" { + depends_on = [aws_instance.api_server] + + triggers = { + script_hash = sha256(templatefile("${path.module}/../../scripts/side_infra_setup.sh.tftpl", { + work_dir = var.work_dir + alloy_env_name = var.alloy_env_name + alloy_config_content = templatefile("${path.module}/../../../../config/side-infra/config.alloy.tftpl", { + loki_ip = data.aws_instance.monitoring_server.private_ip + }) + redis_version = var.redis_version + redis_exporter_version = var.redis_exporter_version + alloy_version = var.alloy_version + })) + } + + provisioner "local-exec" { + interpreter = ["bash", "-c"] + command = <<-EOT + set -euo pipefail + INSTANCE_ID='${aws_instance.api_server.id}' + COMMAND_ID=$(aws ssm send-command \ + --instance-ids "$INSTANCE_ID" \ + --document-name "AWS-RunShellScript" \ + --parameters '${local.side_infra_ssm_params}' \ + --output text \ + --query "Command.CommandId") + ATTEMPTS=0 + while [ "$ATTEMPTS" -lt 360 ]; 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 3600s" >&2 + exit 1 + EOT + } +} diff --git a/modules/app_stack/modules/app/provider.tf b/modules/app_stack/modules/app/provider.tf new file mode 100644 index 0000000..e8f500a --- /dev/null +++ b/modules/app_stack/modules/app/provider.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + cloudinit = { + source = "hashicorp/cloudinit" + version = "~> 2.3" + } + null = { + source = "hashicorp/null" + version = ">= 3.0" + } + } +} diff --git a/modules/app_stack/modules/app/variables.tf b/modules/app_stack/modules/app/variables.tf new file mode 100644 index 0000000..adc740f --- /dev/null +++ b/modules/app_stack/modules/app/variables.tf @@ -0,0 +1,56 @@ +variable "env_name" { + type = string +} + +variable "ami_id" { + type = string +} + +variable "instance_type" { + type = string +} + +variable "api_security_group_id" { + type = string +} + +variable "ec2_iam_instance_profile" { + type = string + default = null +} + +variable "key_name" { + type = string +} + +variable "domain_name" { + type = string +} + +variable "cert_email" { + type = string +} + +variable "nginx_conf_name" { + type = string +} + +variable "work_dir" { + type = string +} + +variable "alloy_env_name" { + type = string +} + +variable "redis_version" { + type = string +} + +variable "redis_exporter_version" { + type = string +} + +variable "alloy_version" { + type = string +} diff --git a/modules/app_stack/modules/db_ec2/main.tf b/modules/app_stack/modules/db_ec2/main.tf new file mode 100644 index 0000000..0236ae0 --- /dev/null +++ b/modules/app_stack/modules/db_ec2/main.tf @@ -0,0 +1,75 @@ +data "cloudinit_config" "db_init" { + count = var.enabled ? 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) + db_data_volume_id = var.data_volume_id + mysql_config_content = file("${path.module}/../../templates/mysql_tuning.cnf") + }) + filename = "mysql_setup.sh" + } +} + +resource "aws_instance" "db_server" { + count = var.enabled ? 1 : 0 + + ami = var.db_ami_id + instance_type = var.db_instance_type + subnet_id = var.db_subnet_id + # 인스턴스를 교체해도 Parameter Store 의 datasource 주소와 API 서버 설정을 그대로 쓰도록 주소를 고정합니다. + private_ip = var.db_private_ip + + vpc_security_group_ids = [ + var.db_security_group_id, + var.alarm_client_security_group_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 = 8 + 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 { + # AMI 갱신이 운영 중인 DB EC2를 교체하지 않도록 무시하고, 인스턴스가 재생성되는 시점에 새 AMI를 적용합니다. + ignore_changes = [ + ami, + key_name, + user_data, + user_data_base64, + user_data_replace_on_change, + ] + } +} + +resource "aws_volume_attachment" "db_data" { + count = var.enabled ? 1 : 0 + + device_name = "/dev/sdf" + volume_id = var.data_volume_id + instance_id = aws_instance.db_server[count.index].id + stop_instance_before_detaching = true +} diff --git a/modules/app_stack/modules/db_ec2/outputs.tf b/modules/app_stack/modules/db_ec2/outputs.tf new file mode 100644 index 0000000..6293821 --- /dev/null +++ b/modules/app_stack/modules/db_ec2/outputs.tf @@ -0,0 +1,7 @@ +output "private_ip" { + value = try(aws_instance.db_server[0].private_ip, null) +} + +output "instance_id" { + value = try(aws_instance.db_server[0].id, null) +} diff --git a/modules/app_stack/modules/db_ec2/provider.tf b/modules/app_stack/modules/db_ec2/provider.tf new file mode 100644 index 0000000..3506dfd --- /dev/null +++ b/modules/app_stack/modules/db_ec2/provider.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + cloudinit = { + source = "hashicorp/cloudinit" + version = "~> 2.3" + } + } +} diff --git a/modules/app_stack/modules/db_ec2/variables.tf b/modules/app_stack/modules/db_ec2/variables.tf new file mode 100644 index 0000000..17e17ff --- /dev/null +++ b/modules/app_stack/modules/db_ec2/variables.tf @@ -0,0 +1,61 @@ +variable "enabled" { + type = bool + default = true +} + +variable "env_name" { + type = string +} + +variable "db_instance_type" { + type = string + default = null +} + +variable "db_ami_id" { + type = string + default = null +} + +variable "db_subnet_id" { + type = string + default = null +} + +variable "db_private_ip" { + type = string + default = null +} + +variable "data_volume_id" { + type = string + default = null +} + +variable "db_security_group_id" { + type = string + default = null +} + +variable "alarm_client_security_group_id" { + type = string + default = null +} + +variable "ec2_iam_instance_profile" { + type = string + default = null +} + +variable "key_name" { + type = string +} + +variable "db_username" { + type = string +} + +variable "db_password" { + type = string + sensitive = true +} diff --git a/modules/app_stack/moved.tf b/modules/app_stack/moved.tf new file mode 100644 index 0000000..a2234cd --- /dev/null +++ b/modules/app_stack/moved.tf @@ -0,0 +1,46 @@ +# app_stack 을 호환 wrapper로 유지하면서 기존 state 주소를 하위 모듈로 이전합니다. +# 이 선언 덕분에 모듈 분리는 실제 EC2나 null_resource를 다시 만들지 않습니다. +moved { + from = data.aws_instance.monitoring_server + to = module.app.data.aws_instance.monitoring_server +} + +moved { + from = data.cloudinit_config.app_init + to = module.app.data.cloudinit_config.app_init +} + +moved { + from = aws_instance.api_server + to = module.app.aws_instance.api_server +} + +moved { + from = null_resource.install_nginx + to = module.app.null_resource.install_nginx +} + +moved { + from = null_resource.update_nginx_conf + to = module.app.null_resource.update_nginx_conf +} + +moved { + from = null_resource.update_side_infra + to = module.app.null_resource.update_side_infra +} + +moved { + from = data.cloudinit_config.db_init[0] + to = module.db_ec2.data.cloudinit_config.db_init[0] +} + +moved { + from = aws_instance.db_server[0] + to = module.db_ec2.aws_instance.db_server[0] +} + +moved { + from = aws_volume_attachment.db_data[0] + to = module.db_ec2.aws_volume_attachment.db_data[0] +} diff --git a/modules/app_stack/output.tf b/modules/app_stack/output.tf index e9e15cf..c129ccd 100644 --- a/modules/app_stack/output.tf +++ b/modules/app_stack/output.tf @@ -1,11 +1,11 @@ output "db_server_private_ip" { description = "DB EC2 서버 private IP" - value = try(aws_instance.db_server[0].private_ip, null) + value = module.db_ec2.private_ip } output "db_server_instance_id" { description = "DB EC2 서버 인스턴스 ID" - value = try(aws_instance.db_server[0].id, null) + value = module.db_ec2.instance_id } output "db_server_data_volume_id" {