Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/secrets
15 changes: 8 additions & 7 deletions environment/monitoring/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
7 changes: 6 additions & 1 deletion environment/monitoring/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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" {
Expand Down
136 changes: 104 additions & 32 deletions modules/app_stack/scripts/nginx_setup.sh.tftpl
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOF > /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_EOF > $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 <<EOF > /etc/nginx/sites-available/$CONF_NAME
write_full_nginx_conf() {
cat <<EOF > /etc/nginx/sites-available/$CONF_NAME
map \$http_upgrade \$connection_upgrade {
default upgrade;
'' '';
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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_EOF > $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 <<EOF > /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!"
86 changes: 80 additions & 6 deletions modules/monitoring_stack/ec2.tf
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,17 +27,13 @@ data "cloudinit_config" "app_init" {
# [Part 2] Nginx 설정 스크립트 파일 생성 (실행 안 함, 파일만 생성)
part {
content_type = "text/cloud-config"
content = <<EOF
content = <<EOF
write_files:
- path: /home/ubuntu/setup_nginx.sh
owner: ubuntu:ubuntu
permissions: '0755'
content: |
${indent(6, templatefile("${path.module}/scripts/nginx_setup.sh.tftpl", {
domain_name = var.domain_name
email = var.cert_email
conf_file_name = var.nginx_conf_name
}))}
${indent(6, local.nginx_setup_script)}
EOF
}
}
Expand All @@ -33,6 +44,7 @@ resource "aws_instance" "monitoring_server" {
key_name = var.key_name

associate_public_ip_address = true
iam_instance_profile = var.ec2_iam_instance_profile

vpc_security_group_ids = [aws_security_group.monitoring_sg.id]

Expand All @@ -54,3 +66,65 @@ resource "aws_instance" "monitoring_server" {
]
}
}

resource "terraform_data" "update_nginx" {
count = var.ec2_iam_instance_profile == null ? 0 : 1

depends_on = [aws_instance.monitoring_server]

triggers_replace = {
instance_id = aws_instance.monitoring_server.id
script_hash = sha256(local.nginx_setup_script)
}

provisioner "local-exec" {
interpreter = ["bash", "-c"]
command = <<-EOT
set -euo pipefail
INSTANCE_ID='${aws_instance.monitoring_server.id}'
ATTEMPTS=0
while [ "$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 "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
}
}
Loading
Loading