到目前為止,我們已經建立了堅實的基礎設施和完整的平台服務,今天開始要邁向最上層的 Application Layer。這一層是使用者最直接接觸的部分,包含 Web 應用、API 服務和背景工作。由於內容龐大,我會分成兩天來分享:今天先聚焦在 Cloud Run 服務的 Terraform 管理,以及它如何與之前建立的平台資源整合;明天則會再繼續練習微服務架構與背景任務的完整實作,分兩部分內容比較不會那麼多啦哈哈!
Application Layer :
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
首先建立 CI/CD 基礎設施,因為其他服務需要從這裡部署。
# 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}"
}
# 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
}
# 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 }
)
}
# 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"
}
# 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
}
# 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 微服務的架構設計與部署,以及報表生成、資料清理等背景任務的實作。