diff --git a/.github/workflows/Backend-CD.yml b/.github/workflows/Backend-CD.yml index c133841..c49e44b 100644 --- a/.github/workflows/Backend-CD.yml +++ b/.github/workflows/Backend-CD.yml @@ -249,6 +249,7 @@ jobs: HEALTH_PORT="${{ env.HEALTH_CHECK_PORT }}" NET="${{ env.DOCKER_NETWORK }}" ENV_FILE="/tmp/relife.env" + DOCKER_PROJECT_DIR="/dockerProjects/relife_app" echo "๐Ÿ”น Use image: ${IMAGE}" if ! docker pull "${IMAGE}"; then @@ -317,21 +318,28 @@ jobs: # --------------------------------------------------------- # 4) green ์—ญํ•  ์ปจํ…Œ์ด๋„ˆ ์žฌ๊ธฐ๋™ # - ๊ฐ™์€ ๋„คํŠธ์›Œํฌ ์ƒ์—์„œ NPM์ด ์ปจํ…Œ์ด๋„ˆ๋ช…:PORT๋กœ ํ”„๋ก์‹œํ•˜๋ฏ€๋กœ -p ๋ถˆํ•„์š” + # - ํ˜ธ์ŠคํŠธ ๋ณผ๋ฅจ ๋งˆ์šดํŠธ๋กœ Blue/Green ๊ฐ„ ๋ฐ์ดํ„ฐ ์˜์†์„ฑ ๋ณด์žฅ # --------------------------------------------------------- docker rm -f "${GREEN}" >/dev/null 2>&1 || true + + # ๋ณผ๋ฅจ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ (์—†์œผ๋ฉด) + mkdir -p ${DOCKER_PROJECT_DIR}/{data,logs} + echo "๐Ÿš€ run new container โ†’ ${GREEN}" docker run -d --name "${GREEN}" \ --restart unless-stopped \ --network "${NET}" \ --env-file "${ENV_FILE}" \ -e TZ=Asia/Seoul \ + -v ${DOCKER_PROJECT_DIR}/data:/app/data \ + -v ${DOCKER_PROJECT_DIR}/logs:/app/logs \ "${IMAGE}" # --------------------------------------------------------- # 5) ํ—ฌ์Šค์ฒดํฌ (/actuator/health/readiness 200 OK๊นŒ์ง€ ๋Œ€๊ธฐ) # --------------------------------------------------------- echo "โฑ health-check: ${GREEN}" - TIMEOUT=120 + TIMEOUT=180 INTERVAL=3 ELAPSED=0 sleep 8 # ์ดˆ๊ธฐ ๋ถ€ํŒ… ์—ฌ์œ  @@ -350,7 +358,7 @@ jobs: if [[ "${CODE:-000}" != "200" ]]; then echo "โŒ ${GREEN} health failed" - docker logs --tail=200 "${GREEN}" || true + docker logs --tail=300 "${GREEN}" || true docker rm -f "${GREEN}" || true exit 1; fi @@ -371,7 +379,7 @@ jobs: else echo "โŒ Failed to switch upstream! Received HTTP status code: ${HTTP_CODE}" # [์ˆ˜์ •] ์ „ํ™˜ ์‹คํŒจ ์‹œ, ์ƒˆ๋กœ ๋„์šด Green ์ปจํ…Œ์ด๋„ˆ๋Š” ์ฆ‰์‹œ ์ œ๊ฑฐํ•˜๊ณ  ์‹คํŒจ ์ฒ˜๋ฆฌ - docker logs --tail=200 "${GREEN}" || true + docker logs --tail=300 "${GREEN}" || true docker rm -f "${GREEN}" || true exit 1 fi diff --git a/back/docker-compose-dev.yml b/back/docker-compose-dev.yml index dd0d312..36c8510 100644 --- a/back/docker-compose-dev.yml +++ b/back/docker-compose-dev.yml @@ -14,7 +14,7 @@ services: environment: TZ: "Asia/Seoul" volumes: - - /dockerProjects/redis_1/volumes/data:/data + - ~/dockerProjects/redis_1/volumes/data:/data networks: common: diff --git a/infra/aws/terraform/cloudfront.tf b/infra/aws/terraform/cloudfront.tf index 672a6df..e742851 100644 --- a/infra/aws/terraform/cloudfront.tf +++ b/infra/aws/terraform/cloudfront.tf @@ -31,6 +31,7 @@ resource "aws_cloudfront_distribution" "cloudfront_distribution" { cached_methods = ["GET", "HEAD"] target_origin_id = "S3-${aws_s3_bucket.s3_1.id}" viewer_protocol_policy = "redirect-to-https" + compress = true # Gzip/Brotli ์••์ถ• ํ™œ์„ฑํ™” forwarded_values { query_string = false @@ -39,10 +40,10 @@ resource "aws_cloudfront_distribution" "cloudfront_distribution" { } } - # ์บ์‹œ ์„ค์ • + # ์บ์‹œ ์„ค์ • - ์ •์  ๋ฆฌ์†Œ์Šค๋Š” ๊ธธ๊ฒŒ min_ttl = 0 - default_ttl = 86400 - max_ttl = 31536000 + default_ttl = 86400 # 1์ผ + max_ttl = 31536000 # 1๋…„ } restrictions { @@ -51,9 +52,22 @@ resource "aws_cloudfront_distribution" "cloudfront_distribution" { } } + # ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์˜ค๋ธŒ์ ํŠธ ์š”์ฒญ ์‹œ custom_error_response { - error_caching_min_ttl = 600 error_code = 404 + error_caching_min_ttl = 300 # 5๋ถ„ (์—๋Ÿฌ ์บ์‹ฑ์œผ๋กœ Origin ๋ถ€ํ•˜ ๊ฐ์†Œ) + } + + # ๊ถŒํ•œ ์—†์Œ ์—๋Ÿฌ (์˜ˆ: S3 ํผ๋ฏธ์…˜ ๋ฌธ์ œ) + custom_error_response { + error_code = 403 + error_caching_min_ttl = 120 + } + + # S3 ์„œ๋น„์Šค ์žฅ์•  ์‹œ + custom_error_response { + error_code = 503 + error_caching_min_ttl = 10 } # ๊ณผ๊ธˆ ์ •์ฑ… @@ -62,15 +76,15 @@ resource "aws_cloudfront_distribution" "cloudfront_distribution" { # PriceClass_All: ์ „์„ธ๊ณ„ price_class = "PriceClass_100" - # fixme: CDN ๋„๋ฉ”์ธ ์„ค์ • - # aliases = [var.cdn_domain] + # CDN ๋„๋ฉ”์ธ ์„ค์ • + aliases = [var.cdn_domain] viewer_certificate { - cloudfront_default_certificate = true - # fixme: ACM ์ธ์ฆ์„œ ์‚ฌ์šฉ(CDN ๋„๋ฉ”์ธ ์„ค์ •) ์‹œ ์•„๋ž˜ ์ฃผ์„ ํ•ด์ œ - # acm_certificate_arn = aws_acm_certificate.cdn_domain_cert.arn - # ssl_support_method = "sni-only" - # minimum_protocol_version = "TLSv1.2_2021" + # cloudfront_default_certificate = true + # ACM ์ธ์ฆ์„œ ์‚ฌ์šฉ(CDN ๋„๋ฉ”์ธ ์„ค์ •) ์‹œ ์•„๋ž˜ ์ฃผ์„ ํ•ด์ œ + acm_certificate_arn = aws_acm_certificate_validation.cdn_domain_cert_validation.certificate_arn + ssl_support_method = "sni-only" + minimum_protocol_version = "TLSv1.2_2021" } tags = merge(local.common_tags, { @@ -82,17 +96,26 @@ resource "aws_cloudfront_distribution" "cloudfront_distribution" { # ACM ์ธ์ฆ์„œ ################## # CloudFront๋Š” us-east-1 ๋ฆฌ์ „์— ์žˆ์–ด์•ผ ํ•จ -# fixme: CDN ๋„๋ฉ”์ธ ์„ค์ • ์‹œ ์ฃผ์„ ํ•ด์ œ -# resource "aws_acm_certificate" "cdn_domain_cert" { -# provider = "us-east-1" -# domain_name = var.cdn_domain -# validation_method = "DNS" -# -# lifecycle { -# create_before_destroy = true -# } -# -# tags = merge(local.common_tags, { -# Name = "${var.prefix}-cdn-domain-cert" -# }) -# } \ No newline at end of file +resource "aws_acm_certificate" "cdn_domain_cert" { + provider = aws.us_east_1 + domain_name = var.cdn_domain + key_algorithm = "EC_prime256v1" + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } + + tags = merge(local.common_tags, { + Name = "${var.prefix}-cdn-domain-cert" + }) +} + +resource "aws_acm_certificate_validation" "cdn_domain_cert_validation" { + provider = aws.us_east_1 + certificate_arn = aws_acm_certificate.cdn_domain_cert.arn + + timeouts { + create = "1h" + } +} \ No newline at end of file diff --git a/infra/aws/terraform/ec2.tf b/infra/aws/terraform/ec2.tf index 96e8631..79046d2 100644 --- a/infra/aws/terraform/ec2.tf +++ b/infra/aws/terraform/ec2.tf @@ -110,9 +110,10 @@ resource "aws_instance" "ec2_1" { # ๋ฃจํŠธ ๋ณผ๋ฅจ ์„ค์ • root_block_device { - volume_type = "gp2" - volume_size = 30 # ๋ณผ๋ฅจ ํฌ๊ธฐ๋ฅผ 30GB๋กœ ์„ค์ • - encrypted = true + volume_type = "gp2" + volume_size = 30 # ๋ณผ๋ฅจ ํฌ๊ธฐ๋ฅผ 30GB๋กœ ์„ค์ • + encrypted = true + delete_on_termination = false } user_data = local.ec2_user_data diff --git a/infra/aws/terraform/main.tf b/infra/aws/terraform/main.tf index d87aa6d..6bbb283 100644 --- a/infra/aws/terraform/main.tf +++ b/infra/aws/terraform/main.tf @@ -1,5 +1,5 @@ ######################################################## -# ๋งˆ์ง€๋ง‰ ์ˆ˜์ •: 251001 +# ๋งˆ์ง€๋ง‰ ์ˆ˜์ •: 251010 # ์ž‘์„ฑ์ž: gooraeng # # AWS ์ธํ”„๋ผ๋ฅผ ์ฝ”๋“œ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ Terraform ๋ฉ”์ธ ์„ค์ • ํŒŒ์ผ @@ -29,6 +29,12 @@ provider "aws" { region = var.region } +# CloudFront๋Š” us-east-1 ๋ฆฌ์ „์˜ ACM ์ธ์ฆ์„œ๊ฐ€ ํ•„์š” +provider "aws" { + alias = "us_east_1" + region = "us-east-1" +} + ################ # ๋กœ์ปฌ ๋ณ€์ˆ˜ ################ diff --git a/infra/aws/terraform/outputs.tf b/infra/aws/terraform/outputs.tf index 58f1a45..3945af3 100644 --- a/infra/aws/terraform/outputs.tf +++ b/infra/aws/terraform/outputs.tf @@ -33,24 +33,23 @@ output "cloudfront_domain" { ################## # ACM ์ธ์ฆ์„œ ์ถœ๋ ฅ (CDN ๋„๋ฉ”์ธ ์„ค์ • ์‹œ) ################## -# fixme: CDN ๋„๋ฉ”์ธ ์„ค์ • ์‹œ ์ฃผ์„ ํ•ด์ œ -# output "cdn_domain_cert_arn" { -# description = "ACM Certificate ARN" -# value = aws_acm_certificate.cdn_domain_cert.arn -# } -# -# output "cdn_domain_cert_status" { -# description = "Certificate Status" -# value = aws_acm_certificate.cdn_domain_cert.status -# } -# -# output "cdn_domain_cert_validation_records" { -# description = "DNS validation records for certificate" -# value = { -# for dvo in aws_acm_certificate.cdn_domain_cert.domain_validation_options : dvo.domain_name => { -# name = dvo.resource_record_name -# record = dvo.resource_record_value -# type = dvo.resource_record_type -# } -# } -# } \ No newline at end of file +output "cdn_domain_cert_arn" { + description = "ACM Certificate ARN" + value = aws_acm_certificate.cdn_domain_cert.arn +} + +output "cdn_domain_cert_status" { + description = "Certificate Status" + value = aws_acm_certificate.cdn_domain_cert.status +} + +output "cdn_domain_cert_validation_records" { + description = "DNS validation records for certificate" + value = { + for dvo in aws_acm_certificate.cdn_domain_cert.domain_validation_options : dvo.domain_name => { + name = dvo.resource_record_name + record = dvo.resource_record_value + type = dvo.resource_record_type + } + } +} \ No newline at end of file diff --git a/infra/aws/terraform/s3.tf b/infra/aws/terraform/s3.tf index 821927b..1c571c5 100644 --- a/infra/aws/terraform/s3.tf +++ b/infra/aws/terraform/s3.tf @@ -36,37 +36,29 @@ resource "aws_s3_bucket_ownership_controls" "s3_1_ownership" { # S3 ๋ฒ„ํ‚ท ์ •์ฑ… ######################### # CloudFront OAI๊ฐ€ S3 ๋ฒ„ํ‚ท์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ํ—ˆ์šฉ +# EC2 ์ธ์Šคํ„ด์Šค๊ฐ€ ํŒŒ์ผ ์—…๋กœ๋“œ/๊ด€๋ฆฌ ๊ฐ€๋Šฅ +# AWS ๊ณ„์ • ์†Œ์œ ์ž ๋ฐ Admin ์‚ฌ์šฉ์ž ์ ‘๊ทผ ํ—ˆ์šฉ # Presigned URL์„ ํ†ตํ•œ ์ ‘๊ทผ ์ฐจ๋‹จ resource "aws_s3_bucket_policy" "s3_1_policy" { bucket = aws_s3_bucket.s3_1.id policy = jsonencode({ Version = "2012-10-17", Statement = [ - # CloudFront๊ฐ€ S3 ๋ฒ„ํ‚ท์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ํ—ˆ์šฉ + # 1. CloudFront OAI: S3 ๊ฐ์ฒด ์ฝ๊ธฐ (CDN ์ฝ˜ํ…์ธ  ์ œ๊ณต) { Sid = "AllowCloudFrontOAIReadOnly", Effect = "Allow", Principal = { AWS = aws_cloudfront_origin_access_identity.oai_1.iam_arn }, - Action = "s3:GetObject", - Resource = "${aws_s3_bucket.s3_1.arn}/*" - }, - # Presigned URL์„ ํ†ตํ•œ ์ ‘๊ทผ ์ฐจ๋‹จ - { - Sid = "DenyPresignedUrls", - Effect = "Deny", - Principal = "*", - Action = "s3:*", + Action = [ + "s3:GetObject", + "s3:ListBucket" + ], Resource = [ aws_s3_bucket.s3_1.arn, "${aws_s3_bucket.s3_1.arn}/*" - ], - Condition = { - StringLike = { - "s3:authType" = "REST-QUERY-STRING" - } - } + ] } ] }) diff --git a/infra/aws/terraform/variables.tf b/infra/aws/terraform/variables.tf index cdc206e..888daf4 100644 --- a/infra/aws/terraform/variables.tf +++ b/infra/aws/terraform/variables.tf @@ -27,12 +27,11 @@ variable "base_domain" { default = "relife.kr" } -# fixme: CDN ๋„๋ฉ”์ธ ๋ณ€์ˆ˜ -# variable "cdn_domain" { -# description = "cdn domain" -# type = string -# default = "cdn.gooraeng.xyz" -# } +variable "cdn_domain" { + description = "cdn domain" + type = string + default = "cdn.relife.kr" +} variable "encryption_type" { description = "S3 ์•”ํ˜ธํ™” ํƒ€์ž… (AES256, aws:kms)"