3계층 앱

아키텍처

3계층 앱은 바닐라 3계층 애플리케이션으로 설계된 간단한 할일 애플리케이션입니다.

  • 백엔드
    • 데이터베이스 - MySQL - Cloud SQL
    • 캐싱 - Redis - Cloud Memorystore
  • 미들웨어/API
    • 컨테이너 호스팅 API - Golang - Cloud Run
  • 프런트엔드/UI
    • 컨테이너 호스팅 UI - Nginx + HTML/JS/CSS - Cloud Run
  • 배포
    • 지속적 배포 - Cloud Build
    • 보안 비밀 관리 - Cloud Secret Manager

시작하기

Cloud Shell에서 소스 코드의 복사본에 대해 다음 링크를 클릭합니다. 그러면 단일 명령어로 프로젝트에서 애플리케이션의 작업 복사본이 작동합니다.

Cloud Shell에서 열기

GitHub에서 소스 코드 보기


3계층 앱 구성요소

3계층 앱 아키텍처에는 여러 제품이 사용됩니다. 다음은 관련 동영상 링크, 제품 문서 및 대화형 둘러보기를 포함하여 구성 요소에 대한 자세한 정보와 함께 구성요소 목록을 보여줍니다.
동영상 문서 둘러보기
Cloud SQL Cloud SQL은 애플리케이션의 데이터베이스 레이어에 MySQL, SQL Server 또는 Postgres를 제공하는 관리형 SQL입니다.
Cloud Memorystore 관리되는 Redis인 Cloud Memorystore는 애플리케이션에 대한 캐싱 레이어를 제공합니다.
Cloud Run Cloud Run을 사용하면 컨테이너에서 애플리케이션을 실행할 수 있지만 서버리스 방식으로 인스턴스, 프로세스, 메모리 수를 구성할 필요가 없습니다. 컨테이너를 업로드하고 URL을 가져옵니다.
Cloud Build Cloud Build는 컨테이너를 패키지하고 이를 Cloud Run 서비스로 사용할 수 있도록 배포하는 도구입니다.
Secret Manager Cloud Secret Manager는 빌드 프로세스를 위해 애플리케이션에 대해 민감한 세부 정보를 저장합니다.

스크립트

설치 스크립트는 go 및 Terraform CLI 도구로 작성된 실행 파일을 사용하여 빈 프로젝트를 가져오고 여기에 애플리케이션을 설치합니다. 출력은 작동하는 애플리케이션과 부하 분산 IP 주소의 URL입니다.

./main.tf

서비스 사용 설정

Google Cloud 서비스는 기본적으로 프로젝트에서 사용 중지되어 있습니다. ToDo에서는 다음 서비스를 사용 설정해야 합니다.

  • 서비스 네트워킹 및 서버리스 VPC 액세스 - Cloud Run이 비공개 네트워크에서 SQL 및 Redis와 통신하도록 허용하고, API에서 들어오는 외부 호출이 이러한 서버에 액세스하지 못하도록 합니다.
  • Cloud Build - 컨테이너 이미지를 만들고 Cloud Run에 배포합니다.
  • Cloud Memorystore - 애플리케이션의 캐싱 레이어를 제공합니다.
  • Cloud Run - 컨테이너를 호스팅하는 서버리스 도구이며 애플리케이션에 액세스하기 위한 URL을 제공합니다.
  • Cloud SQL - 애플리케이션의 데이터베이스 스토리지입니다.
  • Cloud Storage - Cloud Build에 사용되며, 데이터베이스에 스키마를 로드합니다.
  • Cloud Secret Manager - SQL 및 Redis에 대해 호스트 IP를 Cloud Run용 Cloud Build에 삽입하는 데 사용됩니다.
  • Artifact Registry - Cloud Build에 사용할 Docker 이미지를 저장합니다.
variable "gcp_service_list" {
    description = "The list of apis necessary for the project"
    type        = list(string)
    default = [
        "compute.googleapis.com",
        "cloudapis.googleapis.com",
        "vpcaccess.googleapis.com",
        "servicenetworking.googleapis.com",
        "cloudbuild.googleapis.com",
        "sql-component.googleapis.com",
        "sqladmin.googleapis.com",
        "storage.googleapis.com",
        "secretmanager.googleapis.com",
        "run.googleapis.com",
        "artifactregistry.googleapis.com",
        "redis.googleapis.com"
    ]
}

resource "google_project_service" "all" {
    for_each                   = toset(var.gcp_service_list)
    project                    = var.project_number
    service                    = each.key
    disable_on_destroy = false
}

권한 설정

다음 명령어는 Cloud Build가 서비스를 배포하도록 허용하는 IAM 역할 및 권한을 설정합니다.

  • Cloud Run에 배포하도록 Cloud Build 서비스 계정 사용 설정
  • Cloud Run용 VPN 액세스 설정을 위해 Cloud Build 서비스 계정 사용 설정
  • 서비스 계정 활동 수행을 위해 Cloud Build 서비스 계정 사용 설정
  • Compute 서비스 계정 대신 작동하도록 Cloud Build 서비스 계정 사용 설정
  • Cloud Run에 게시하도록 Cloud Build 서비스 계정 사용 설정
  • 보안 비밀을 사용하도록 Cloud Build 서비스 계정 사용 설정
  • Artifact Registry에 컨테이너를 저장하도록 Cloud Build 서비스 계정 사용 설정
variable "build_roles_list" {
    description = "The list of roles that build needs for"
    type        = list(string)
    default = [
        "roles/run.developer",
        "roles/vpaccess.user",
        "roles/iam.serviceAccountUser",
        "roles/run.admin",
        "roles/secretmanager.secretAccessor",
        "roles/artifactregistry.admin",
    ]
}
resource "google_project_iam_member" "allbuild" {
    for_each   = toset(var.build_roles_list)
    project    = var.project_number
    role       = each.key
    member     = "serviceAccount:${local.sabuild}"
    depends_on = [google_project_service.all]
}

SQL 인스턴스의 네트워킹 만들기

다음 명령어는 Cloud Run에서 Cloud SQL에 액세스하도록 허용합니다.

resource "google_compute_global_address" "google_managed_services_vpn_connector" {
    name          = "google-managed-services-vpn-connector"
    purpose       = "VPC_PEERING"
    address_type  = "INTERNAL"
    prefix_length = 16
    network       = local.defaultnetwork
    project       = var.project_id
    depends_on    = [google_project_service.all]
}
resource "google_service_networking_connection" "vpcpeerings" {
    network                 = local.defaultnetwork
    service                 = "servicenetworking.googleapis.com"
    reserved_peering_ranges = [google_compute_global_address.google_managed_services_vpn_connector.name]
}

VPC 액세스 커넥터 만들기

Cloud Run을 데이터베이스 및 캐싱에 연결

resource "google_vpc_access_connector" "connector" {
    provider      = google-beta
    project       = var.project_id
    name          = "vpc-connector"
    ip_cidr_range = "10.8.0.0/28"
    network       = "default"
    region        = var.region
    depends_on    = [google_compute_global_address.google_managed_services_vpn_connector, google_project_service.all]
}

Redis 서버 만들기

Redis 서버 인스턴스를 구성하고 초기화합니다.

resource "google_redis_instance" "todo_cache" {
    authorized_network      = local.defaultnetwork
    connect_mode            = "DIRECT_PEERING"
    location_id             = var.zone
    memory_size_gb          = 1
    name                    = "${var.basename}-cache"
    project                 = var.project_id
    redis_version           = "REDIS_6_X"
    region                  = var.region
    reserved_ip_range       = "10.137.125.88/29"
    tier                    = "BASIC"
    transit_encryption_mode = "DISABLED"
    depends_on              = [google_project_service.all]
}

SQL 서버 만들기

다음 명령어는 SQL 서버 인스턴스를 구성하고 초기화합니다.

resource "google_sql_database_instance" "todo_database" {
    name="${var.basename}-db-${random_id.id.hex}"
    database_version = "MYSQL_5_7"
    region           = var.region
    project          = var.project_id
    settings {
        tier                  = "db-g1-small"
        disk_autoresize       = true
        disk_autoresize_limit = 0
        disk_size             = 10
        disk_type             = "PD_SSD"
        ip_configuration {
            ipv4_enabled    = false
            private_network = local.defaultnetwork
        }
        location_preference {
            zone = var.zone
        }
    }
    deletion_protection = false
    depends_on = [
        google_project_service.all,
        google_service_networking_connection.vpcpeerings
    ]
    # This handles loading the schema after the database installs.
    provisioner "local-exec" {
        working_dir = "${path.module}/code/database"
        command     = "./load_schema.sh ${var.project_id} ${google_sql_database_instance.todo_database.name}"
    }
}

Artifact Registry 저장소 만들기

다음 명령어는 Cloud Run에 사용할 Docker 이미지를 저장합니다.

resource "google_artifact_registry_repository" "todo_app" {
    provider      = google-beta
    format        = "DOCKER"
    location      = var.region
    project       = var.project_id
    repository_id = "${var.basename}-app"
    depends_on    = [google_project_service.all]
}

보안 비밀 만들기

다음 명령어는 Redis 및 SQL 호스트 데이터를 클라우드 보안 비밀에 저장합니다.

resource "google_secret_manager_secret" "redishost" {
    project = var.project_number
    replication {
        automatic = true
    }
    secret_id  = "redishost"
    depends_on = [google_project_service.all]
}
resource "google_secret_manager_secret_version" "redishost" {
    enabled     = true
    secret      = "projects/${var.project_number}/secrets/redishost"
    secret_data = google_redis_instance.todo_cache.host
    depends_on  = [google_project_service.all, google_redis_instance.todo_cache, google_secret_manager_secret.redishost]
}
resource "google_secret_manager_secret" "sqlhost" {
    project = var.project_number
    replication {
        automatic = true
    }
    secret_id  = "sqlhost"
    depends_on = [google_project_service.all]
}
resource "google_secret_manager_secret_version" "sqlhost" {
    enabled     = true
    secret      = "projects/${var.project_number}/secrets/sqlhost"
    secret_data = google_sql_database_instance.todo_database.private_ip_address
    depends_on  = [google_project_service.all, google_sql_database_instance.todo_database, google_secret_manager_secret.sqlhost]
}

미들웨어용 아티팩트 만들기

다음 명령어는 Docker 이미지를 만들고 Artifact Registry에서 호스팅합니다. ./code/frontend/clouldbuild.yaml

resource "null_resource" "cloudbuild_api" {
  provisioner "local-exec" {
    working_dir = "${path.module}/code/middleware"
    command     = "gcloud builds submit . --substitutions=_REGION=${var.region},_BASENAME=${var.basename}"
  }
  depends_on = [
    google_artifact_registry_repository.todo_app,
    google_secret_manager_secret_version.redishost,
    google_secret_manager_secret_version.sqlhost,
    google_project_service.all
  ]
}

Cloud Run에 API 컨테이너 배포

다음 명령어는 Cloud Build를 사용하고 바로 전에 만든 컨테이너를 사용하여 Cloud Run에서 서비스를 가동합니다.

resource "google_cloud_run_service" "api" {
    name     = "${var.basename}-api"
    location = var.region
    project  = var.project_id

    template {
        spec {
            containers {
                image = "${var.region}-docker.pkg.dev/${var.project_id}/${var.basename}-app/api"
                env {
                    name = "REDISHOST"
                    value_from {
                        secret_key_ref {
                            name = google_secret_manager_secret.redishost.secret_id
                            key  = "latest"
                        }
                    }
                }
                env {
                    name = "todo_host"
                    value_from {
                        secret_key_ref {
                        name = google_secret_manager_secret.sqlhost.secret_id
                        key  = "latest"
                        }
                    }
                }
                env {
                    name  = "todo_user"
                    value = "todo_user"
                }
                env {
                    name  = "todo_pass"
                    value = "todo_pass"
                }
                env {
                    name  = "todo_name"
                    value = "todo"
                }
                env {
                    name  = "REDISPORT"
                    value = "6379"
                }
            }
        }
        metadata {
            annotations = {
                "autoscaling.knative.dev/maxScale"        = "1000"
                "run.googleapis.com/cloudsql-instances"   = google_sql_database_instance.todo_database.connection_name
                "run.googleapis.com/client-name"          = "terraform"
                "run.googleapis.com/vpc-access-egress"    = "all"
                "run.googleapis.com/vpc-access-connector" = google_vpc_access_connector.connector.id
            }
        }
    }
    autogenerate_revision_name = true
    depends_on = [
        null_resource.cloudbuild_api,
        google_project_iam_member.secretmanager_secretAccessor
    ]
} 

전 세계에서 읽을 수 있도록 Cloud Run API 서비스를 엽니다.

애플리케이션의 API 레이어가 사용자의 브라우저를 통해 호출되지만 기본적으로 Cloud Run 서비스는 공개되지 않습니다. 사용자가 이 서비스를 사용하려면 전 세계에서 액세스할 수 있도록 이러한 서비스에 대한 권한을 열어야 합니다.

resource "google_cloud_run_service_iam_policy" "noauth_api" {
    location    = google_cloud_run_service.api.location
    project     = google_cloud_run_service.api.project
    service     = google_cloud_run_service.api.name
    policy_data = data.google_iam_policy.noauth.policy_data
}

프런트엔드용 아티팩트 만들기

다음 명령어는 Docker 이미지를 만들고 Artifact Registry에서 호스팅합니다. ./code/frontend/clouldbuild.yaml

resource "null_resource" "cloudbuild_fe" {
    provisioner "local-exec" {
        working_dir = "${path.module}/code/frontend"
        command     = "gcloud builds submit . --substitutions=_REGION=${var.region},_BASENAME=${var.basename}"
    }
    depends_on = [
        google_artifact_registry_repository.todo_app,
        google_cloud_run_service.api
    ]
}

Cloud Run에 프런트 엔드 컨테이너 배포

다음 명령어는 Cloud Build를 사용하고 바로 전에 빌드한 컨테이너를 사용하여 Cloud Run에서 서비스를 가동합니다.

resource "google_cloud_run_service" "fe" {
    name     = "${var.basename}-fe"
    location = var.region
    project  = var.project_id
    template {
        spec {
            containers {
                image = "${var.region}-docker.pkg.dev/${var.project_id}/${var.basename}-app/fe"
                ports {
                    container_port = 80
                }
            }
        }
    }
    depends_on = [null_resource.cloudbuild_fe]
}

전 세계에서 읽을 수 있도록 Cloud Run 프런트엔드 서비스 열기

애플리케이션의 프런트엔드이며, 사용자가 애플리케이션과 상호작용하는 데 사용되는 HTML/JS/CSS를 렌더링합니다. 기본적으로 Cloud Run 서비스는 공개되지 않습니다. 이 애플리케이션이 작동하려면 누구나 사용할 수 있도록 이러한 서비스에 대해 액세스 권한을 부여해야 합니다.

resource "google_cloud_run_service_iam_policy" "noauth_fe" {
    location    = google_cloud_run_service.fe.location
    project     = google_cloud_run_service.fe.project
    service     = google_cloud_run_service.fe.name
    policy_data = data.google_iam_policy.noauth.policy_data
}

./code/database/load_schema.sh

데이터베이스 스키마 초기화

이 명령어는 스키마를 Cloud SQL에 업로드하기 위해 임시 Cloud Storage 버킷을 만듭니다.

PROJECT=$1
SQLNAME=$2

SQLSERVICEACCOUNT=$(gcloud sql instances describe $SQLNAME --format="value(serviceAccountEmailAddress)" | xargs)
gcloud storage buckets create gs://$PROJECT-temp
gcloud storage cp schema.sql gs://$PROJECT-temp/schema.sql
gcloud storage buckets add-iam-policy-binding gs://$PROJECT-temp/ --member=serviceAccount:$SQLSERVICEACCOUNT --role=roles/storage.objectViewer
gcloud sql import sql $SQLNAME gs://$PROJECT-temp/schema.sql -q
gcloud storage rm gs://$PROJECT-temp --recursive

./code/middleware/clouldbuild.yaml

Build API 컨테이너

이 코드는 미들웨어 레이어에 대한 Docker 이미지를 만듭니다.

name: 'gcr.io/cloud-builders/docker'
args: [ 'build', '-t', '$_REGION-docker.pkg.dev/$PROJECT_ID/$_BASENAME-app/api', '.' ]
  ```
#### Push API container to Artifact Registry
Pushing the container to Artifact Registry makes it possible for Cloud Run to 
get the image and serve it.

``` yaml
name: 'gcr.io/cloud-builders/docker'
args: ['push', '$_REGION-docker.pkg.dev/$PROJECT_ID/$_BASENAME-app/api']  

대체 항목

다음 코드는 기본 값을 사용하여 변수를 만듭니다. 이러한 값을 배포 시에 변경할 수 있습니다.

substitutions:
  _REGION: us-central1
  _BASENAME: todo

./code/frontend/clouldbuild.yaml

Massage 코드 콘텐츠

프런트엔드는 완전히 정적 HTML/JS/CSS입니다. 앱이 바로 전에 만든 API 서비스의 URL을 가리켜야 하지만 Cloud Run 서비스에는 무작위 문자열이 포함된 URL이 할당됩니다. 'massage 스크립트'는 무작위 URL을 캡처하고 이 컨테이너에서 정적 JS의 코드에 삽입합니다.

./code/frontend/massage.sh

name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: bash
args: [ './massage.sh', '$_REGION' ]

Build API 컨테이너

다음 코드는 미들웨어 레이어에 대한 Docker 이미지를 만듭니다.

name: 'gcr.io/cloud-builders/docker'
args: [ 'build', '-t', '$_REGION-docker.pkg.dev/$PROJECT_ID/$_BASENAME-app/fe', '.' ]

API 컨테이너를 Artifact Registry에 푸시

컨테이너를 Artifact Registry에 푸시하면 Cloud Run이 이미지를 가져와서 제공할 수 있습니다.

name: 'gcr.io/cloud-builders/docker'
args: ['push', '$_REGION-docker.pkg.dev/$PROJECT_ID/$_BASENAME-app/fe']

대체 항목

값을 배포 시에 변경할 수 있도록 기본값을 사용하여 변수를 만듭니다.

substitutions:
  _REGION: us-central1
  _BASENAME: todo

./code/frontend/massage.sh

자바스크립트 수정

이 명령어는 미들웨어에 대해 엔드포인트를 프런트엔드의 자바스크립트에 삽입합니다.

API=$(gcloud run services describe todo-api --region=$1 --format="value(status.url)")
stripped=$(echo ${API/https:\/\//})
sed -i"" -e "s/127.0.0.1:9000/$stripped/" www/js/main.js

결론

이제 프로젝트에서 Cloud Run에서 실행되는 간단한 3계층 할일 애플리케이션이 만들어졌습니다. 또한 환경에 맞게 이 솔루션을 수정하거나 확장할 수 있도록 모든 코드가 준비되었습니다.