diff --git a/.github/workflows/terraform-plan.yml b/.github/workflows/terraform-plan.yml index 04a54ec..9114cf6 100644 --- a/.github/workflows/terraform-plan.yml +++ b/.github/workflows/terraform-plan.yml @@ -15,18 +15,16 @@ permissions: pull-requests: write id-token: write -# 환경변수 설정 -env: - GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} - GCP_SERVICE_ACCOUNT: ${{ secrets.GCP_SERVICE_ACCOUNT }} - TERRAFORM_STATE_BUCKET: ${{ secrets.TERRAFORM_STATE_BUCKET }} - TERRAFORM_TFVARS_PROD: ${{ secrets.TERRAFORM_TFVARS_PROD }} - jobs: # 변경된 Terraform 파일을 기준으로 영향받는 환경을 계산 detect-changes: name: 변경 환경 감지 - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 + env: + GCP_WORKLOAD_IDENTITY_PROVIDER: "" + GCP_SERVICE_ACCOUNT: "" + TERRAFORM_STATE_BUCKET: "" + TERRAFORM_TFVARS_PROD: "" outputs: environments: ${{ steps.detect.outputs.environments || '[]' }} steps: @@ -45,41 +43,81 @@ jobs: echo "변경된 파일 목록:" echo "$CHANGED_FILES" - # 변경된 환경을 JSON 배열 문자열로 수집합니다. - ENVIRONMENTS="" - - # prod 환경 파일 변경 여부를 확인합니다. + # 변경된 환경 목록을 배열로 수집합니다. + ENVIRONMENTS=() if echo "$CHANGED_FILES" | grep -q "terraform/environments/prod/"; then - ENVIRONMENTS="$ENVIRONMENTS\"prod\"," + ENVIRONMENTS+=("prod") fi - # 공통 모듈이 바뀌면 모든 환경에서 Plan을 수행합니다. if echo "$CHANGED_FILES" | grep -q "terraform/modules/"; then - ENVIRONMENTS="\"prod\"" + ENVIRONMENTS+=("prod") + fi + + # 중복을 제거한 뒤 JSON 배열 문자열로 변환합니다. + if [ ${#ENVIRONMENTS[@]} -eq 0 ]; then + ENVIRONMENTS_JSON='[]' + else + ENVIRONMENTS_JSON=$(printf '%s\n' "${ENVIRONMENTS[@]}" | sort -u | jq -R . | jq -cs .) fi - # 마지막 쉼표를 제거하고 JSON 배열 형식으로 변환합니다. - ENVIRONMENTS=$(echo "$ENVIRONMENTS" | sed 's/,$//') - ENVIRONMENTS="[$ENVIRONMENTS]" + echo "감지된 환경: $ENVIRONMENTS_JSON" + echo "environments=$ENVIRONMENTS_JSON" >> "$GITHUB_OUTPUT" + + # detect-changes 출력값을 바로 확인하기 위한 디버그 잡 + debug-detect-changes: + name: 디버그 - 감지 결과 + runs-on: ubuntu-22.04 + needs: detect-changes + if: always() + env: + GCP_WORKLOAD_IDENTITY_PROVIDER: "" + GCP_SERVICE_ACCOUNT: "" + TERRAFORM_STATE_BUCKET: "" + TERRAFORM_TFVARS_PROD: "" - echo "감지된 환경: $ENVIRONMENTS" - echo "environments=$ENVIRONMENTS" >> $GITHUB_OUTPUT + steps: + - name: detect-changes 출력 확인 + env: + ENVIRONMENTS: ${{ needs.detect-changes.outputs.environments }} + run: | + echo "detect-changes environments 원본: $ENVIRONMENTS" + printf 'detect-changes environments quoted: <%s>\n' "$ENVIRONMENTS" + echo "detect-changes environments 길이: ${#ENVIRONMENTS}" + echo "detect-changes environments 바이트:" + printf '%s' "$ENVIRONMENTS" | od -An -tx1 + + echo "detect-changes environments JSON 파싱 시도:" + if [ -n "$ENVIRONMENTS" ]; then + printf '%s\n' "$ENVIRONMENTS" | jq -c . + else + echo "값이 비어 있습니다." + fi # 환경별 Terraform init/fmt/validate/plan을 수행하고 결과 파일을 아티팩트로 남깁니다. terraform-plan-checks: name: 체크 - ${{ matrix.environment }} - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 needs: detect-changes if: (needs.detect-changes.outputs.environments || '[]') != '[]' # 변경된 환경이 있을 때만 실행합니다. + env: + GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + GCP_SERVICE_ACCOUNT: ${{ secrets.GCP_SERVICE_ACCOUNT }} + TERRAFORM_STATE_BUCKET: ${{ secrets.TERRAFORM_STATE_BUCKET }} + TERRAFORM_TFVARS_PROD: ${{ secrets.TERRAFORM_TFVARS_PROD }} strategy: fail-fast: false # 한 환경 실패가 다른 환경 확인을 막지 않도록 유지합니다. matrix: - environment: ${{ fromJson(needs.detect-changes.outputs.environments || '[]') }} # 감지된 환경별로 병렬 실행합니다. + environment: ${{ fromJson(needs.detect-changes.outputs.environments || '[]') }} steps: - name: 코드 체크아웃 uses: actions/checkout@v4 + - name: Matrix 디버그 + run: | + echo "matrix.environment=${{ matrix.environment }}" + echo "needs.detect-changes.outputs.environments=${{ needs.detect-changes.outputs.environments }}" + # 출력 파일 기반으로 결과를 다루기 위해 wrapper를 비활성화 - name: Terraform 설치 uses: hashicorp/setup-terraform@v3 @@ -120,16 +158,8 @@ jobs: if: steps.init.outcome == 'success' working-directory: terraform/environments/${{ matrix.environment }} run: | - case "${{ matrix.environment }}" in - prod) - TFVARS_CONTENT="${TERRAFORM_TFVARS_PROD}" - SECRET_NAME="TERRAFORM_TFVARS_PROD" - ;; - *) - echo "오류: 지원하지 않는 환경입니다: ${{ matrix.environment }}" - exit 1 - ;; - esac + TFVARS_CONTENT="${TERRAFORM_TFVARS_PROD}" + SECRET_NAME="TERRAFORM_TFVARS_PROD" if [ -z "${TFVARS_CONTENT}" ]; then echo "오류: GitHub Secret ${SECRET_NAME}를 설정해야 합니다." @@ -194,16 +224,13 @@ jobs: # 업로드된 결과 파일을 읽어 PR 댓글을 환경별로 갱신 comment-plan-results: name: PR 댓글 갱신 - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 needs: - detect-changes - terraform-plan-checks if: always() && (needs.detect-changes.outputs.environments || '[]') != '[]' && github.event_name == 'pull_request' - - # 특정 환경 댓글 실패가 다른 환경 댓글을 막지 않도록 유지 strategy: fail-fast: false - # 감지된 환경별로 댓글을 분리 matrix: environment: ${{ fromJson(needs.detect-changes.outputs.environments || '[]') }} diff --git a/terraform/environments/prod/compute.tf b/terraform/environments/prod/compute.tf index 83dbf16..9868db6 100644 --- a/terraform/environments/prod/compute.tf +++ b/terraform/environments/prod/compute.tf @@ -24,42 +24,27 @@ module "web_servers" { # 관리형 인스턴스 그룹을 사용하지 않을 때만 개별 인스턴스를 정의합니다. instances = !var.use_instance_group && var.create_web_instances ? tomap({ web1 = { - name = "${var.environment}-web-01" - zone = "${var.region}-a" - machine_type = var.web_machine_type - enable_external_ip = false # 프로덕션 환경은 로드 밸런서를 통한 접근만 허용합니다. - tags = ["web-server", var.environment] - labels = { - environment = var.environment - role = "web" - } + name = "${var.environment}-web-01" + zone = "${var.region}-a" + machine_type = var.web_machine_type + enable_external_ip = false # 프로덕션 환경은 로드 밸런서를 통한 접근만 허용합니다. + tags = ["web-server", var.environment] deletion_protection = true # 실수로 삭제되지 않도록 보호합니다. } - web2 = { - name = "${var.environment}-web-02" - zone = "${var.region}-b" - machine_type = var.web_machine_type - enable_external_ip = false - tags = ["web-server", var.environment] - labels = { - environment = var.environment - role = "web" - } - deletion_protection = true - } }) : tomap({}) # 공통 인스턴스 설정 machine_type = var.web_machine_type source_image = var.web_source_image - boot_disk_size_gb = 50 - boot_disk_type = "pd-ssd" # 프로덕션 환경은 SSD 디스크를 사용합니다. + boot_disk_size_gb = var.web_machine_ssd + boot_disk_type = "pd-ssd" enable_external_ip = false tags = ["web-server", var.environment] - labels = { - environment = var.environment - role = "web" - } + + # 태그 + common_tags = merge(var.common_tags, { + Service = "Backend" + }) # 서비스 계정 설정 service_account_email = var.service_account_email diff --git a/terraform/environments/prod/load-balancer.tf b/terraform/environments/prod/load-balancer.tf index e357df1..10638c2 100644 --- a/terraform/environments/prod/load-balancer.tf +++ b/terraform/environments/prod/load-balancer.tf @@ -25,11 +25,15 @@ module "load_balancer" { backend_timeout_sec = 30 session_affinity = "CLIENT_IP" backend_groups = var.use_instance_group ? [ - { - group = module.web_servers.instance_group_instance_group - balancing_mode = "UTILIZATION" - max_utilization = 0.8 - } + merge( + { + group = module.web_servers.instance_group_instance_group + balancing_mode = var.lb_type == "NETWORK" ? "CONNECTION" : "UTILIZATION" + }, + var.lb_type == "NETWORK" ? {} : { + max_utilization = 0.8 + } + ) ] : [] ssl_certificates = var.ssl_certificates diff --git a/terraform/environments/prod/storage.tf b/terraform/environments/prod/storage.tf index a893bfa..1b125e5 100644 --- a/terraform/environments/prod/storage.tf +++ b/terraform/environments/prod/storage.tf @@ -6,16 +6,17 @@ module "storage" { project_id = var.project_id default_location = var.storage_location - default_labels = { - environment = var.environment - managed_by = "terraform" - } + + # 태그 + common_tags = merge(var.common_tags, { + Service = "Storage" + }) # 버킷 정의 buckets = merge( var.create_storage_buckets ? tomap({ static_assets = { - name = "${var.project_id}-${var.environment}-static-assets" + name = "${var.project_id}-${var.environment}" storage_class = "STANDARD" uniform_bucket_level_access = true versioning_enabled = true @@ -45,60 +46,6 @@ module "storage" { } ] : [] } - }) : tomap({}), - var.create_storage_buckets ? tomap({ - backups = { - name = "${var.project_id}-${var.environment}-backups" - storage_class = "NEARLINE" - uniform_bucket_level_access = true - versioning_enabled = true - force_destroy = false - public_access_prevention = "enforced" - - # 오래된 백업은 저장 등급을 낮추고 최종적으로 삭제합니다. - lifecycle_rules = [ - { - action = { - type = "SetStorageClass" - storage_class = "COLDLINE" - } - condition = { - age = 90 - } - }, - { - action = { - type = "Delete" - } - condition = { - age = 365 - num_newer_versions = 10 - } - } - ] - } - }) : tomap({}), - var.create_storage_buckets ? tomap({ - logs = { - name = "${var.project_id}-${var.environment}-logs" - storage_class = "STANDARD" - uniform_bucket_level_access = true - versioning_enabled = false - force_destroy = false - public_access_prevention = "enforced" - - # 로그 버킷은 30일 이후 자동 삭제합니다. - lifecycle_rules = [ - { - action = { - type = "Delete" - } - condition = { - age = 30 - } - } - ] - } }) : tomap({}) ) } diff --git a/terraform/environments/prod/terraform.tfvars.example b/terraform/environments/prod/terraform.tfvars.example index b98ede3..0180814 100644 --- a/terraform/environments/prod/terraform.tfvars.example +++ b/terraform/environments/prod/terraform.tfvars.example @@ -30,7 +30,7 @@ autoscaling_max_replicas = 5 web_machine_type = "e2-standard-2" web_source_image = "ubuntu-os-cloud/ubuntu-2204-lts" - +web_machine_ssd = 30 # ======================================== # 스토리지 관련 값 # ======================================== diff --git a/terraform/environments/prod/variables.tf b/terraform/environments/prod/variables.tf index a13fe2c..3ff61b1 100644 --- a/terraform/environments/prod/variables.tf +++ b/terraform/environments/prod/variables.tf @@ -64,7 +64,7 @@ variable "create_web_instances" { variable "instance_group_size" { description = "관리형 인스턴스 그룹의 목표 인스턴스 수입니다." type = number - default = 2 + default = 1 } variable "enable_autoscaling" { @@ -76,7 +76,7 @@ variable "enable_autoscaling" { variable "autoscaling_min_replicas" { description = "오토스케일링 최소 인스턴스 수입니다." type = number - default = 2 + default = 1 } variable "autoscaling_max_replicas" { @@ -91,6 +91,12 @@ variable "web_machine_type" { default = "e2-standard-2" # 운영 환경에 맞춘 고성능 인스턴스입니다. } +variable "web_machine_ssd" { + description = "웹 서버 부팅 디스크 크기(GB)입니다." + type = number + default = 50 +} + variable "web_source_image" { description = "웹 서버 부팅 디스크에 사용할 이미지입니다." type = string @@ -115,7 +121,7 @@ variable "create_storage_buckets" { variable "storage_location" { description = "스토리지 버킷을 생성할 위치입니다." type = string - default = "ASIA" # 운영 환경에서는 멀티 리전을 기본값으로 사용합니다. + default = "ASIA-NORTHEAST3" # 비용 절감을 위해 단일 리전을 기본값으로 사용합니다. } variable "allowed_cors_origins" { @@ -144,3 +150,17 @@ variable "ssl_certificates" { type = list(string) default = [] } + +# ======================================== +# 공통 태그 변수 +# ======================================== +variable "common_tags" { + description = "공통 태그입니다." + type = map(string) + default = { + project = "pinhouse" + environment = "prod" + version = "v1" + managed_by = "terraform" + } +} diff --git a/terraform/environments/prod/vpc.tf b/terraform/environments/prod/vpc.tf index f43d0f4..a874a2c 100644 --- a/terraform/environments/prod/vpc.tf +++ b/terraform/environments/prod/vpc.tf @@ -6,7 +6,7 @@ module "vpc" { vpc_name = "${var.project}-${var.environment}-vpc" description = "프로덕션 환경용 VPC 네트워크" - routing_mode = "GLOBAL" + routing_mode = "REGIONAL" # 서브넷 정의 subnets = { diff --git a/terraform/modules/compute/main.tf b/terraform/modules/compute/main.tf index a639930..ed50b39 100644 --- a/terraform/modules/compute/main.tf +++ b/terraform/modules/compute/main.tf @@ -52,8 +52,10 @@ resource "google_compute_instance_template" "template" { # 네트워크 태그입니다. tags = var.tags - # 공통 레이블입니다. - labels = var.labels + # 공통 태그를 GCP 레이블에 반영합니다. + labels = { + for k, v in var.common_tags : lower(k) => lower(v) + } # 선점형 인스턴스일 때 스케줄링 정책을 조정합니다. scheduling { @@ -107,11 +109,11 @@ resource "google_compute_instance" "instances" { enable-oslogin = var.enable_os_login }, var.metadata, - lookup(each.value, "metadata", {}) + coalesce(lookup(each.value, "metadata", null), {}) ) # 인스턴스별 시작 스크립트를 우선 적용합니다. - metadata_startup_script = lookup(each.value, "startup_script", var.startup_script) + metadata_startup_script = coalesce(lookup(each.value, "startup_script", null), var.startup_script) # 서비스 계정 설정입니다. service_account { @@ -120,10 +122,17 @@ resource "google_compute_instance" "instances" { } # 공통 태그와 인스턴스별 태그를 합칩니다. - tags = concat(var.tags, lookup(each.value, "tags", [])) + tags = concat(var.tags, coalesce(lookup(each.value, "tags", null), [])) - # 공통 레이블과 인스턴스별 레이블을 합칩니다. - labels = merge(var.labels, lookup(each.value, "labels", {})) + # 공통 태그와 인스턴스별 태그 맵을 GCP 레이블에 반영합니다. + labels = merge( + { + for k, v in var.common_tags : lower(k) => lower(v) + }, + { + for k, v in coalesce(lookup(each.value, "common_tags", null), {}) : lower(k) => lower(v) + } + ) # 선점형 여부에 따라 스케줄링 정책을 조정합니다. scheduling { diff --git a/terraform/modules/compute/variables.tf b/terraform/modules/compute/variables.tf index 25975d5..2c4ec06 100644 --- a/terraform/modules/compute/variables.tf +++ b/terraform/modules/compute/variables.tf @@ -109,8 +109,8 @@ variable "tags" { default = [] } -variable "labels" { - description = "인스턴스에 적용할 공통 레이블입니다." +variable "common_tags" { + description = "인스턴스에 적용할 공통 태그입니다." type = map(string) default = {} } @@ -135,7 +135,7 @@ variable "instances" { metadata = optional(map(string)) startup_script = optional(string) tags = optional(list(string)) - labels = optional(map(string)) + common_tags = optional(map(string)) deletion_protection = optional(bool) })) default = {} diff --git a/terraform/modules/load-balancer/main.tf b/terraform/modules/load-balancer/main.tf index e0b5c71..907d218 100644 --- a/terraform/modules/load-balancer/main.tf +++ b/terraform/modules/load-balancer/main.tf @@ -127,6 +127,15 @@ resource "google_compute_region_backend_service" "backend_service" { # 선택적으로 연결 드레이닝 타임아웃을 적용합니다. connection_draining_timeout_sec = var.connection_draining_timeout + + lifecycle { + precondition { + condition = alltrue([ + for backend in var.backend_groups : lookup(backend, "balancing_mode", "CONNECTION") == "CONNECTION" + ]) + error_message = "NETWORK 로드 밸런서의 backend_groups balancing_mode는 CONNECTION만 사용할 수 있습니다." + } + } } # ======================================== diff --git a/terraform/modules/load-balancer/variables.tf b/terraform/modules/load-balancer/variables.tf index da8833d..581b1eb 100644 --- a/terraform/modules/load-balancer/variables.tf +++ b/terraform/modules/load-balancer/variables.tf @@ -84,9 +84,14 @@ variable "health_check_ids" { # 백엔드 서비스 변수 # ======================================== variable "backend_protocol" { - description = "백엔드 서비스 프로토콜입니다. TCP, UDP, SSL 중 하나를 사용합니다." + description = "백엔드 서비스 프로토콜입니다. TCP, UDP, UNSPECIFIED 중 하나를 사용합니다." type = string default = "TCP" + + validation { + condition = contains(["TCP", "UDP", "UNSPECIFIED"], var.backend_protocol) + error_message = "backend_protocol은 TCP, UDP, UNSPECIFIED 중 하나여야 합니다." + } } variable "backend_timeout_sec" { diff --git a/terraform/modules/nat-instance/main.tf b/terraform/modules/nat-instance/main.tf index 5412a66..9dd8e8f 100644 --- a/terraform/modules/nat-instance/main.tf +++ b/terraform/modules/nat-instance/main.tf @@ -60,8 +60,10 @@ resource "google_compute_instance" "nat_instance" { } } - tags = concat(var.tags, [var.nat_instance_tag]) - labels = var.labels + tags = concat(var.tags, [var.nat_instance_tag]) + labels = { + for k, v in var.common_tags : lower(k) => lower(v) + } allow_stopping_for_update = true } diff --git a/terraform/modules/nat-instance/variables.tf b/terraform/modules/nat-instance/variables.tf index 0caab88..06912ea 100644 --- a/terraform/modules/nat-instance/variables.tf +++ b/terraform/modules/nat-instance/variables.tf @@ -105,8 +105,8 @@ variable "tags" { default = [] } -variable "labels" { - description = "NAT 인스턴스에 부여할 레이블입니다." +variable "common_tags" { + description = "NAT 인스턴스에 부여할 공통 태그입니다." type = map(string) default = {} } diff --git a/terraform/modules/storage/main.tf b/terraform/modules/storage/main.tf index 0f8414b..cba1fc3 100644 --- a/terraform/modules/storage/main.tf +++ b/terraform/modules/storage/main.tf @@ -11,10 +11,14 @@ resource "google_storage_bucket" "buckets" { # 버킷이 생성될 프로젝트입니다. project = var.project_id - # 기본 레이블과 버킷별 레이블을 병합합니다. + # 기본 태그와 버킷별 태그를 GCP 레이블에 반영합니다. labels = merge( - var.default_labels, - lookup(each.value, "labels", {}) + { + for k, v in var.common_tags : lower(k) => lower(v) + }, + { + for k, v in coalesce(lookup(each.value, "common_tags", null), {}) : lower(k) => lower(v) + } ) # 균일한 버킷 수준 액세스 설정입니다. diff --git a/terraform/modules/storage/variables.tf b/terraform/modules/storage/variables.tf index 6e77f53..83bf49b 100644 --- a/terraform/modules/storage/variables.tf +++ b/terraform/modules/storage/variables.tf @@ -23,8 +23,8 @@ variable "default_storage_class" { } } -variable "default_labels" { - description = "모든 버킷에 공통 적용할 레이블입니다." +variable "common_tags" { + description = "모든 버킷에 공통 적용할 태그입니다." type = map(string) default = {} } @@ -38,7 +38,7 @@ variable "buckets" { name = string location = optional(string) storage_class = optional(string) - labels = optional(map(string)) + common_tags = optional(map(string)) uniform_bucket_level_access = optional(bool) versioning_enabled = optional(bool) force_destroy = optional(bool)