diff --git a/config/secrets b/config/secrets index ad87e45..14aa1cf 160000 --- a/config/secrets +++ b/config/secrets @@ -1 +1 @@ -Subproject commit ad87e450f7a7e9b718ed9c1cc32908884011f80c +Subproject commit 14aa1cfa4161c55b2548cc62aa54d98bd3a21484 diff --git a/environment/monitoring/main.tf b/environment/monitoring/main.tf index 778cd4d..2b74f8b 100644 --- a/environment/monitoring/main.tf +++ b/environment/monitoring/main.tf @@ -7,20 +7,21 @@ module "monitoring_stack" { # 기존 app_stack 모듈을 재사용하거나, 모니터링 전용 모듈이 있다면 경로 수정 source = "../../modules/monitoring_stack" - env_name = "monitoring" - vpc_id = data.aws_vpc.default.id + env_name = "monitoring" + vpc_id = data.aws_vpc.default.id - ami_id = var.ami_id + ami_id = var.ami_id - key_name = var.key_name + key_name = var.key_name + ec2_iam_instance_profile = var.ec2_iam_instance_profile - instance_type = var.monitoring_instance_type + instance_type = var.monitoring_instance_type private_ip = var.private_ip # Nginx 및 도메인 설정 - domain_name = var.domain_name - cert_email = var.cert_email + domain_name = var.domain_name + cert_email = var.cert_email nginx_conf_name = var.nginx_conf_name diff --git a/environment/monitoring/variables.tf b/environment/monitoring/variables.tf index b22341c..723dac1 100644 --- a/environment/monitoring/variables.tf +++ b/environment/monitoring/variables.tf @@ -13,6 +13,11 @@ variable "key_name" { type = string } +variable "ec2_iam_instance_profile" { + description = "EC2 instance profile name for SSM RunCommand" + type = string +} + variable "monitoring_ingress_rules" { description = "Ingress rules for Grafana(3000), Prometheus(9090), Loki(3100)" type = list(object({ @@ -26,7 +31,7 @@ variable "monitoring_ingress_rules" { variable "private_ip" { description = "Fixed private ip for alloy config" - type = string + type = string } variable "domain_name" { diff --git a/modules/app_stack/scripts/nginx_setup.sh.tftpl b/modules/app_stack/scripts/nginx_setup.sh.tftpl index f9e1211..6bd8808 100644 --- a/modules/app_stack/scripts/nginx_setup.sh.tftpl +++ b/modules/app_stack/scripts/nginx_setup.sh.tftpl @@ -6,44 +6,52 @@ set -e DOMAIN="${domain_name}" EMAIL="${email}" CONF_NAME="${conf_file_name}" +CRON_NAME=$(printf '%s' "$CONF_NAME" | tr -c 'A-Za-z0-9_-' '-') UPSTREAM_CONF="/etc/nginx/conf.d/upstream.conf" +CERTBOT_WEBROOT="/var/www/certbot" +CERT_FULLCHAIN="/etc/letsencrypt/live/$DOMAIN/fullchain.pem" +CERT_PRIVKEY="/etc/letsencrypt/live/$DOMAIN/privkey.pem" echo "Start Nginx Setup for $DOMAIN with config file: $CONF_NAME" -# 1. Install necessary packages for Nginx and Certbot -apt-get update -apt-get install -y nginx python3 python3-venv libaugeas0 +if [ -z "$CRON_NAME" ]; then + CRON_NAME="nginx" +fi -# 2. Install Certbot (using pip) -python3 -m venv /opt/certbot/ -/opt/certbot/bin/pip install --upgrade pip -/opt/certbot/bin/pip install certbot certbot-nginx -ln -sf /opt/certbot/bin/certbot /usr/bin/certbot +reload_nginx() { + nginx -t + systemctl reload nginx || systemctl restart nginx +} -# 3. Issue SSL certificate (Non-interactive mode) -systemctl stop nginx +write_http_challenge_conf() { + cat < /etc/nginx/sites-available/$CONF_NAME +# 1차 차단: 도메인과 일치하지 않는 요청 차단 (IP 직접 접근, 알 수 없는 Host 헤더) +# 응답 없이 연결을 즉시 종료하여 봇이 서버 존재를 인식하지 못하게 함 +server { + listen 80 default_server; + server_name _; + return 444; +} -certbot certonly --standalone \ - --non-interactive \ - --agree-tos \ - --email "$EMAIL" \ - -d "$DOMAIN" +server { + listen 80; + server_name $DOMAIN; -echo "Certificate obtained successfully." + location ^~ /.well-known/acme-challenge/ { + root $CERTBOT_WEBROOT; + default_type "text/plain"; + try_files \$uri =404; + } -# 4. Create upstream config file only on first provisioning (initial active slot: blue on port 8080) -# Blue-Green 배포 시 이 파일만 교체하고 nginx -s reload 로 트래픽 전환 -# 이미 존재하면 덮어쓰지 않음 — 재프로비저닝 시 현재 active 슬롯 유지 -if [ ! -f "$UPSTREAM_CONF" ]; then - cat < $UPSTREAM_CONF -upstream app_backend { - server 127.0.0.1:8080; + location / { + return 301 https://\$host\$request_uri; + } +} +EOF } -UPSTREAM_EOF -fi -# 5. Create Nginx configuration file -cat < /etc/nginx/sites-available/$CONF_NAME +write_full_nginx_conf() { + cat < /etc/nginx/sites-available/$CONF_NAME map \$http_upgrade \$connection_upgrade { default upgrade; '' ''; @@ -70,8 +78,15 @@ server { server { listen 80; server_name $DOMAIN; + + location ^~ /.well-known/acme-challenge/ { + root $CERTBOT_WEBROOT; + default_type "text/plain"; + try_files \$uri =404; + } + location / { - return 301 https://\$host\$request_uri; + return 301 https://\$host\$request_uri; } } @@ -115,16 +130,73 @@ server { } } EOF +} + +# 1. Install necessary packages for Nginx and Certbot +apt-get update +apt-get install -y nginx python3 python3-venv libaugeas0 + +# 2. Install Certbot (using pip) +python3 -m venv /opt/certbot/ +/opt/certbot/bin/pip install --upgrade pip +/opt/certbot/bin/pip install certbot certbot-nginx +ln -sf /opt/certbot/bin/certbot /usr/bin/certbot + +# 3. Prepare Nginx webroot for HTTP-01 challenge +mkdir -p "$CERTBOT_WEBROOT" + +# Create upstream config file only on first provisioning (initial active slot: blue on port 8080) +# Blue-Green 배포 시 이 파일만 교체하고 nginx -s reload 로 트래픽 전환 +# 이미 존재하면 덮어쓰지 않음 — 재프로비저닝 시 현재 active 슬롯 유지 +if [ ! -f "$UPSTREAM_CONF" ]; then + cat < $UPSTREAM_CONF +upstream app_backend { + server 127.0.0.1:8080; +} +UPSTREAM_EOF +fi + +CERT_EXISTS=false +if [ -f "$CERT_FULLCHAIN" ] && [ -f "$CERT_PRIVKEY" ]; then + CERT_EXISTS=true + write_full_nginx_conf +else + write_http_challenge_conf +fi -# 6. Create symbolic link and remove default configuration ln -sf /etc/nginx/sites-available/$CONF_NAME /etc/nginx/sites-enabled/$CONF_NAME rm -f /etc/nginx/sites-enabled/default +reload_nginx + +# 4. Issue or renew SSL certificate (Non-interactive mode) +certbot certonly --webroot \ + --webroot-path "$CERTBOT_WEBROOT" \ + --non-interactive \ + --agree-tos \ + --keep-until-expiring \ + --email "$EMAIL" \ + -d "$DOMAIN" + +echo "Certificate obtained successfully." + +# 5. Create Nginx configuration file +if [ "$CERT_EXISTS" != "true" ]; then + write_full_nginx_conf + + # 6. Create symbolic link and remove default configuration + ln -sf /etc/nginx/sites-available/$CONF_NAME /etc/nginx/sites-enabled/$CONF_NAME + rm -f /etc/nginx/sites-enabled/default +fi # 7. Register auto-renewal cron job -echo "0 0,12 * * * root /opt/certbot/bin/python -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew -q" | tee -a /etc/crontab > /dev/null +cat < /etc/cron.d/certbot-$CRON_NAME +SHELL=/bin/bash +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + +0 0,12 * * * root /opt/certbot/bin/python -c 'import random,time; time.sleep(random.random() * 3600)' && /usr/bin/certbot renew --webroot --webroot-path $CERTBOT_WEBROOT --quiet --deploy-hook "/usr/bin/systemctl reload nginx" +EOF # 8. Nginx restart -nginx -t -systemctl restart nginx +reload_nginx echo "Nginx setup complete!" diff --git a/modules/monitoring_stack/ec2.tf b/modules/monitoring_stack/ec2.tf index 9de07dd..e7f5d88 100644 --- a/modules/monitoring_stack/ec2.tf +++ b/modules/monitoring_stack/ec2.tf @@ -1,3 +1,18 @@ +locals { + nginx_setup_script = templatefile("${path.module}/scripts/nginx_setup.sh.tftpl", { + domain_name = var.domain_name + email = var.cert_email + conf_file_name = var.nginx_conf_name + }) + + nginx_script_b64 = base64encode(local.nginx_setup_script) + + nginx_ssm_params = jsonencode({ + commands = ["cloud-init status --wait > /dev/null", "echo ${local.nginx_script_b64} | base64 -d | sudo bash"] + executionTimeout = ["3600"] + }) +} + data "cloudinit_config" "app_init" { gzip = true base64_encode = true @@ -12,17 +27,13 @@ data "cloudinit_config" "app_init" { # [Part 2] Nginx 설정 스크립트 파일 생성 (실행 안 함, 파일만 생성) part { content_type = "text/cloud-config" - content = </dev/null || echo "Offline") + if [ "$PING_STATUS" = "Online" ]; then + break + fi + ATTEMPTS=$((ATTEMPTS + 1)) + sleep 10 + done + if [ "$PING_STATUS" != "Online" ]; then + echo "SSM agent did not become online after 600s" >&2 + exit 1 + fi + + COMMAND_ID=$(aws ssm send-command \ + --instance-ids "$INSTANCE_ID" \ + --document-name "AWS-RunShellScript" \ + --parameters '${local.nginx_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/monitoring_stack/scripts/nginx_setup.sh.tftpl b/modules/monitoring_stack/scripts/nginx_setup.sh.tftpl index 251860c..3bb994e 100644 --- a/modules/monitoring_stack/scripts/nginx_setup.sh.tftpl +++ b/modules/monitoring_stack/scripts/nginx_setup.sh.tftpl @@ -5,32 +5,53 @@ set -e DOMAIN="${domain_name}" EMAIL="${email}" CONF_NAME="${conf_file_name}" +CRON_NAME=$(printf '%s' "$CONF_NAME" | tr -c 'A-Za-z0-9_-' '-') +CERTBOT_WEBROOT="/var/www/certbot" +CERT_FULLCHAIN="/etc/letsencrypt/live/$DOMAIN/fullchain.pem" +CERT_PRIVKEY="/etc/letsencrypt/live/$DOMAIN/privkey.pem" -echo ">>> [수동 실행] $DOMAIN 에 대한 Nginx 및 SSL 설정을 시작합니다." +echo ">>> $DOMAIN 에 대한 Nginx 및 SSL 설정을 시작합니다." -# 1. 패키지 설치 -sudo apt-get update -sudo apt-get install -y nginx python3 python3-venv libaugeas0 +if [ -z "$CRON_NAME" ]; then + CRON_NAME="nginx" +fi -# 2. Certbot 설치 및 링크 -sudo python3 -m venv /opt/certbot/ -sudo /opt/certbot/bin/pip install --upgrade pip -sudo /opt/certbot/bin/pip install certbot certbot-nginx -sudo ln -sf /opt/certbot/bin/certbot /usr/bin/certbot +reload_nginx() { + sudo nginx -t + sudo systemctl reload nginx || sudo systemctl restart nginx +} -# 3. SSL 인증서 발급 -sudo systemctl stop nginx -sudo certbot certonly --standalone \ - --non-interactive \ - --agree-tos \ - --email "$EMAIL" \ - -d "$DOMAIN" +write_http_challenge_conf() { + sudo tee /etc/nginx/sites-available/$CONF_NAME > /dev/null < /dev/null < /dev/null </dev/null | grep -q "certbot renew"; then - (crontab -l 2>/dev/null; echo "0 0,12 * * * /usr/bin/certbot renew --quiet --post-hook 'systemctl reload nginx'") | crontab - +# 3. HTTP-01 challenge를 nginx가 처리할 수 있도록 webroot 설정 +sudo mkdir -p "$CERTBOT_WEBROOT" + +CERT_EXISTS=false +if [ -f "$CERT_FULLCHAIN" ] && [ -f "$CERT_PRIVKEY" ]; then + CERT_EXISTS=true + write_full_nginx_conf +else + write_http_challenge_conf fi -# 7. Nginx 재시작 -sudo nginx -t -sudo systemctl restart nginx +activate_nginx_conf +reload_nginx + +# 4. SSL 인증서 발급/갱신 +sudo certbot certonly --webroot \ + --webroot-path "$CERTBOT_WEBROOT" \ + --non-interactive \ + --agree-tos \ + --keep-until-expiring \ + --email "$EMAIL" \ + -d "$DOMAIN" + +# 5. Nginx 설정 파일 작성 +if [ "$CERT_EXISTS" != "true" ]; then + write_full_nginx_conf + + # 6. 설정 활성화 + activate_nginx_conf +fi + +# 7. 자동 갱신 크론 등록 +sudo tee /etc/cron.d/certbot-$CRON_NAME > /dev/null <