iT邦幫忙

2025 iThome 鐵人賽

DAY 28
0
DevOps

30 天 Terraform 學習筆記:從零開始的 IaC 實戰系列 第 28

Day 28 - Terraform 實戰練習 : Application Layer Pt.1

  • 分享至 

  • xImage
  •  

到目前為止,我們已經建立了堅實的基礎設施和完整的平台服務,今天開始要邁向最上層的 Application Layer。這一層是使用者最直接接觸的部分,包含 Web 應用、API 服務和背景工作。由於內容龐大,我會分成兩天來分享:今天先聚焦在 Cloud Run 服務的 Terraform 管理,以及它如何與之前建立的平台資源整合;明天則會再繼續練習微服務架構與背景任務的完整實作,分兩部分內容比較不會那麼多啦哈哈!

今天的實作目標

Application Layer :

  • Web 應用程式 (前端 + API Gateway)
  • CI/CD 管線 (Artifact Registry、Cloud Build Triggers)

API 微服務以及背景工作這兩個部分我會留在明天~

專案結構

applications/
├── main.tf                    # 主要應用編排
├── variables.tf               # 應用變數
├── outputs.tf                 # 應用輸出
├── web-app/                   # 前端應用
│   ├── main.tf               # Web 應用配置
│   ├── load-balancer.tf      # 負載平衡器
│   ├── variables.tf
│   └── outputs.tf
└── cicd/                     # CI/CD 管線
    ├── artifact-registry.tf  # 容器倉庫
    ├── cloud-build.tf       # 建置觸發器
    ├── variables.tf
    └── outputs.tf

Part 1: CI/CD 基礎設施

首先建立 CI/CD 基礎設施,因為其他服務需要從這裡部署。

1.1 Artifact Registry

# applications/cicd/artifact-registry.tf
# Docker 映像倉庫
resource "google_artifact_registry_repository" "docker_repo" {
  location      = var.region
  repository_id = "${var.environment}-docker-repo"
  description   = "Docker repository for ${var.environment} environment"
  format        = "DOCKER"
  
  labels = {
    environment = var.environment
    service     = "cicd"
    tier        = "application"
  }
}

# 給 Cloud Build 服務帳號推送權限
resource "google_artifact_registry_repository_iam_member" "build_writer" {
  project    = var.project_id
  location   = google_artifact_registry_repository.docker_repo.location
  repository = google_artifact_registry_repository.docker_repo.name
  role       = "roles/artifactregistry.writer"
  member     = "serviceAccount:${data.terraform_remote_state.infrastructure.outputs.build_service_account_email}"
}

# 給應用服務帳號讀取權限
resource "google_artifact_registry_repository_iam_member" "app_reader" {
  project    = var.project_id
  location   = google_artifact_registry_repository.docker_repo.location
  repository = google_artifact_registry_repository.docker_repo.name
  role       = "roles/artifactregistry.reader"
  member     = "serviceAccount:${data.terraform_remote_state.infrastructure.outputs.app_service_account_email}"
}

1.2 Cloud Build Triggers

# applications/cicd/cloud-build.tf
# Web 應用建置觸發器
resource "google_cloudbuild_trigger" "webapp_build" {
  name        = "${var.environment}-webapp-build"
  description = "Build and deploy web application"
  
  # GitHub 觸發器設定
  github {
    owner = var.github_config.owner
    name  = var.github_config.repo_name
    
    push {
      branch = var.environment == "production" ? "^main$" : "^develop$"
    }
  }
  
  # 建置步驟
  build {
    # 建置 Docker 映像
    step {
      name = "gcr.io/cloud-builders/docker"
      args = [
        "build",
        "-t", "${var.region}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.docker_repo.repository_id}/webapp:$COMMIT_SHA",
        "-t", "${var.region}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.docker_repo.repository_id}/webapp:latest",
        "--file", "apps/webapp/Dockerfile",
        "."
      ]
    }
    
    # 推送映像到 Artifact Registry
    step {
      name = "gcr.io/cloud-builders/docker"
      args = [
        "push", 
        "${var.region}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.docker_repo.repository_id}/webapp:$COMMIT_SHA"
      ]
    }
    
    step {
      name = "gcr.io/cloud-builders/docker"
      args = [
        "push",
        "${var.region}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.docker_repo.repository_id}/webapp:latest"
      ]
    }
    
    # 部署到 Cloud Run
    step {
      name = "gcr.io/cloud-builders/gcloud"
      args = [
        "run", "deploy", "${var.environment}-webapp",
        "--image", "${var.region}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.docker_repo.repository_id}/webapp:$COMMIT_SHA",
        "--region", var.region,
        "--platform", "managed",
        "--allow-unauthenticated"
      ]
    }
    
    # 設定環境變數
    step {
      name = "gcr.io/cloud-builders/gcloud"
      args = [
        "run", "services", "update", "${var.environment}-webapp",
        "--region", var.region,
        "--set-env-vars", join(",", [
          "ENVIRONMENT=${var.environment}",
          "PROJECT_ID=${var.project_id}",
          "DATABASE_SECRET_NAME=${data.terraform_remote_state.infrastructure.outputs.secret_names.database_url}",
          "REDIS_SECRET_NAME=${data.terraform_remote_state.platform.outputs.redis_connection_secret}"
        ])
      ]
    }
  }
  
  # 設定服務帳號
  service_account = data.terraform_remote_state.infrastructure.outputs.build_service_account_email
}

# API 服務建置觸發器
resource "google_cloudbuild_trigger" "api_services_build" {
  for_each = toset(["order-service", "payment-service", "notification-service"])
  
  name        = "${var.environment}-${each.key}-build"
  description = "Build and deploy ${each.key}"
  
  github {
    owner = var.github_config.owner
    name  = var.github_config.repo_name
    
    push {
      branch = var.environment == "production" ? "^main$" : "^develop$"
    }
  }
  
  # 只在相關檔案變更時觸發
  included_files = ["apps/${each.key}/**"]
  
  build {
    step {
      name = "gcr.io/cloud-builders/docker"
      args = [
        "build",
        "-t", "${var.region}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.docker_repo.repository_id}/${each.key}:$COMMIT_SHA",
        "-t", "${var.region}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.docker_repo.repository_id}/${each.key}:latest",
        "--file", "apps/${each.key}/Dockerfile",
        "."
      ]
    }
    
    step {
      name = "gcr.io/cloud-builders/docker"
      args = [
        "push",
        "${var.region}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.docker_repo.repository_id}/${each.key}:$COMMIT_SHA"
      ]
    }
    
    step {
      name = "gcr.io/cloud-builders/gcloud"
      args = [
        "run", "deploy", "${var.environment}-${each.key}",
        "--image", "${var.region}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.docker_repo.repository_id}/${each.key}:$COMMIT_SHA",
        "--region", var.region,
        "--platform", "managed",
        "--no-allow-unauthenticated"
      ]
    }
  }
  
  service_account = data.terraform_remote_state.infrastructure.outputs.build_service_account_email
}

1.3 CI/CD 變數和輸出

# applications/cicd/variables.tf
variable "project_id" {
  description = "GCP Project ID"
  type        = string
}

variable "region" {
  description = "GCP region"
  type        = string
}

variable "environment" {
  description = "Environment name"
  type        = string
}

variable "github_config" {
  description = "GitHub repository configuration"
  type = object({
    owner     = string
    repo_name = string
  })
}

# applications/cicd/outputs.tf
output "docker_repository_url" {
  description = "Docker repository URL"
  value       = "${var.region}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.docker_repo.repository_id}"
}

output "build_trigger_names" {
  description = "Cloud Build trigger names"
  value = merge(
    { webapp = google_cloudbuild_trigger.webapp_build.name },
    { for k, v in google_cloudbuild_trigger.api_services_build : k => v.name }
  )
}

Part 2: Web 應用程式

2.1 Cloud Run Web 服務

# applications/web-app/main.tf
# Web 應用程式 Cloud Run 服務
resource "google_cloud_run_service" "webapp" {
  name     = "${var.environment}-webapp"
  location = var.region
  
  template {
    metadata {
      labels = {
        environment = var.environment
        service     = "webapp"
        tier        = "application"
      }
      
      annotations = {
        # 自動擴展設定
        "autoscaling.knative.dev/minScale" = var.webapp_config.min_instances
        "autoscaling.knative.dev/maxScale" = var.webapp_config.max_instances
        
        # 資源限制
        "run.googleapis.com/cpu-throttling" = "false"
        "run.googleapis.com/execution-environment" = "gen2"
        
        # VPC 連接器 (如果需要存取私有資源)
        "run.googleapis.com/vpc-access-connector" = data.terraform_remote_state.infrastructure.outputs.vpc_connector_name
        "run.googleapis.com/vpc-access-egress" = "private-ranges-only"
      }
    }
    
    spec {
      container_concurrency = var.webapp_config.concurrency
      timeout_seconds      = var.webapp_config.timeout_seconds
      service_account_name = data.terraform_remote_state.infrastructure.outputs.app_service_account_email
      
      containers {
        # 初始映像 (將由 Cloud Build 更新)
        image = "${data.terraform_remote_state.applications.outputs.docker_repository_url}/webapp:latest"
        
        # 資源配置
        resources {
          limits = {
            cpu    = var.webapp_config.cpu_limit
            memory = var.webapp_config.memory_limit
          }
        }
        
        # 環境變數
        env {
          name  = "ENVIRONMENT"
          value = var.environment
        }
        
        env {
          name  = "PROJECT_ID"
          value = var.project_id
        }
        
        env {
          name  = "REGION"
          value = var.region
        }
        
        # 從 Secret Manager 讀取資料庫連線
        env {
          name = "DATABASE_URL"
          value_from {
            secret_key_ref {
              name = data.terraform_remote_state.infrastructure.outputs.secret_names.database_url
              key  = "latest"
            }
          }
        }
        
        # Redis 連線
        env {
          name = "REDIS_URL"
          value_from {
            secret_key_ref {
              name = data.terraform_remote_state.platform.outputs.redis_connection_secret
              key  = "latest"
            }
          }
        }
        
        # JWT 金鑰
        env {
          name = "JWT_SECRET"
          value_from {
            secret_key_ref {
              name = data.terraform_remote_state.infrastructure.outputs.secret_names.jwt_secret
              key  = "latest"
            }
          }
        }
        
        # 健康檢查
        liveness_probe {
          http_get {
            path = "/health"
            port = 8080
          }
          initial_delay_seconds = 30
          timeout_seconds      = 5
          period_seconds       = 10
          failure_threshold    = 3
        }
        
        startup_probe {
          http_get {
            path = "/ready"
            port = 8080
          }
          initial_delay_seconds = 10
          timeout_seconds      = 5
          period_seconds       = 5
          failure_threshold    = 30
        }
      }
    }
  }
  
  traffic {
    percent         = 100
    latest_revision = true
  }
  
  lifecycle {
    ignore_changes = [
      template[0].spec[0].containers[0].image,
      template[0].metadata[0].annotations["run.googleapis.com/operation-id"]
    ]
  }
}

# 允許未認證存取 (公開 Web 應用)
resource "google_cloud_run_service_iam_member" "webapp_public_access" {
  service  = google_cloud_run_service.webapp.name
  location = google_cloud_run_service.webapp.location
  role     = "roles/run.invoker"
  member   = "allUsers"
}

2.2 負載平衡器

# applications/web-app/load-balancer.tf
# 後端服務
resource "google_compute_backend_service" "webapp_backend" {
  name                    = "${var.environment}-webapp-backend"
  description             = "Backend service for web application"
  protocol                = "HTTP"
  port_name               = "http"
  timeout_sec             = 30
  enable_cdn              = var.environment == "production"
  
  backend {
    group = google_compute_region_network_endpoint_group.webapp_neg.id
  }
  
  health_checks = [google_compute_health_check.webapp_health.id]
  
  # CDN 設定 (生產環境)
  dynamic "cdn_policy" {
    for_each = var.environment == "production" ? [1] : []
    
    content {
      cache_mode        = "CACHE_ALL_STATIC"
      signed_url_cache_max_age_sec = 3600
      default_ttl       = 3600
      max_ttl           = 86400
      client_ttl        = 3600
      negative_caching  = true
      
      cache_key_policy {
        include_host         = true
        include_protocol     = true
        include_query_string = false
      }
    }
  }
  
  log_config {
    enable      = true
    sample_rate = var.environment == "production" ? 0.1 : 1.0
  }
}

# 網路端點群組 (Cloud Run)
resource "google_compute_region_network_endpoint_group" "webapp_neg" {
  name                  = "${var.environment}-webapp-neg"
  network_endpoint_type = "SERVERLESS"
  region                = var.region
  
  cloud_run {
    service = google_cloud_run_service.webapp.name
  }
}

# 健康檢查
resource "google_compute_health_check" "webapp_health" {
  name        = "${var.environment}-webapp-health"
  description = "Health check for web application"
  
  timeout_sec         = 5
  check_interval_sec  = 30
  healthy_threshold   = 1
  unhealthy_threshold = 3
  
  http_health_check {
    port               = "80"
    request_path       = "/health"
    proxy_header       = "NONE"
  }
}

# URL 對應
resource "google_compute_url_map" "webapp_url_map" {
  name            = "${var.environment}-webapp-url-map"
  description     = "URL map for web application"
  default_service = google_compute_backend_service.webapp_backend.id
  
  # API 路由
  path_matcher {
    name            = "api-routes"
    default_service = google_compute_backend_service.webapp_backend.id
    
    path_rule {
      paths   = ["/api/orders/*"]
      service = google_compute_backend_service.order_service_backend.id
    }
    
    path_rule {
      paths   = ["/api/payments/*"]
      service = google_compute_backend_service.payment_service_backend.id
    }
    
    path_rule {
      paths   = ["/api/notifications/*"]
      service = google_compute_backend_service.notification_service_backend.id
    }
  }
  
  host_rule {
    hosts        = var.custom_domain != null ? [var.custom_domain] : ["*"]
    path_matcher = "api-routes"
  }
}

# HTTPS 代理
resource "google_compute_target_https_proxy" "webapp_https_proxy" {
  name   = "${var.environment}-webapp-https-proxy"
  url_map = google_compute_url_map.webapp_url_map.id
  
  ssl_certificates = var.custom_domain != null ? [
    data.terraform_remote_state.infrastructure.outputs.ssl_certificate_id
  ] : [google_compute_managed_ssl_certificate.default_ssl[0].id]
}

# 預設 SSL 憑證 (如果沒有自定義網域)
resource "google_compute_managed_ssl_certificate" "default_ssl" {
  count = var.custom_domain == null ? 1 : 0
  
  name = "${var.environment}-default-ssl"
  
  managed {
    domains = ["${var.environment}-webapp.example.com"]
  }
}

# 全域轉發規則
resource "google_compute_global_forwarding_rule" "webapp_https_forwarding" {
  name       = "${var.environment}-webapp-https-forwarding"
  target     = google_compute_target_https_proxy.webapp_https_proxy.id
  port_range = "443"
  ip_address = data.terraform_remote_state.infrastructure.outputs.load_balancer_ip
}

# HTTP 到 HTTPS 重新導向
resource "google_compute_url_map" "https_redirect" {
  name = "${var.environment}-https-redirect"
  
  default_url_redirect {
    https_redirect         = true
    redirect_response_code = "MOVED_PERMANENTLY_DEFAULT"
    strip_query            = false
  }
}

resource "google_compute_target_http_proxy" "http_proxy" {
  name   = "${var.environment}-http-proxy"
  url_map = google_compute_url_map.https_redirect.id
}

resource "google_compute_global_forwarding_rule" "http_forwarding" {
  name       = "${var.environment}-http-forwarding"
  target     = google_compute_target_http_proxy.http_proxy.id
  port_range = "80"
  ip_address = data.terraform_remote_state.infrastructure.outputs.load_balancer_ip
}

2.3 Web 應用變數和輸出

# applications/web-app/variables.tf
variable "project_id" {
  description = "GCP Project ID"
  type        = string
}

variable "region" {
  description = "GCP region"
  type        = string
}

variable "environment" {
  description = "Environment name"
  type        = string
}

variable "webapp_config" {
  description = "Web application configuration"
  type = object({
    min_instances    = string
    max_instances    = string
    cpu_limit        = string
    memory_limit     = string
    concurrency      = number
    timeout_seconds  = number
  })
}

variable "custom_domain" {
  description = "Custom domain for the application"
  type        = string
  default     = null
}

# applications/web-app/outputs.tf
output "webapp_url" {
  description = "Web application URL"
  value       = google_cloud_run_service.webapp.status[0].url
}

output "load_balancer_ip" {
  description = "Load balancer IP address"
  value       = data.terraform_remote_state.infrastructure.outputs.load_balancer_ip
}

output "service_name" {
  description = "Cloud Run service name"
  value       = google_cloud_run_service.webapp.name
}

總結一下

今天我們成功完成了 Application Layer 的前半部分,為應用程式部署打下了關鍵基礎。透過 Artifact Registry 和 Cloud Build Triggers,我們建立了安全可靠的 CI/CD 流程,讓多個服務能自動化建置與部署。Web 應用程式則已順利運行在 Cloud Run 上,搭配負載平衡器、SSL 憑證與 CDN,不僅能承載生產級流量,還能兼顧高效能與安全性。整個部署過程中,我們也確保了環境變數透過 Secret Manager 管理,並整合 VPC 私有網路、自動擴展、監控與日誌等功能,讓系統具備企業級的完整性與可靠度。

明天我們將進一步完成 Application Layer 的另一半重點:包含訂單、支付、通知等 API 微服務的架構設計與部署,以及報表生成、資料清理等背景任務的實作。


上一篇
Day 27 - Terraform 實作練習:Platform Layer
下一篇
Day 29 - Terraform 實戰練習:Application Layer Pt.2
系列文
30 天 Terraform 學習筆記:從零開始的 IaC 實戰29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言