modules/web_hosting/main.tf (432 lines of code) (raw):
/**
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
locals {
random_id = var.deployment_id != null ? var.deployment_id : random_id.default.0.hex
project = (var.create_project
? try(module.project_radlab_web_hosting.0, null)
: try(data.google_project.existing_project.0, null)
)
default_apis = [
"compute.googleapis.com",
"iap.googleapis.com",
"networkmanagement.googleapis.com",
"servicenetworking.googleapis.com",
"sqladmin.googleapis.com",
]
project_services = var.enable_services ? (var.billing_budget_pubsub_topic ? distinct(concat(local.default_apis, [
"pubsub.googleapis.com"
])) : local.default_apis) : []
web_startup_script = templatefile("${path.module}/scripts/build/startup_scripts/sample_app/sample_webapp.sh.tpl", {
INSTANCE_CONNECTION_NAME = module.sql_db_postgresql.instance_connection_name
CLOUD_SQL_DATABASE_NAME = "postgres"
CLOUD_SQL_USERNAME = module.sql_db_postgresql.additional_users[0].name
CLOUD_SQL_PASSWORD = module.sql_db_postgresql.additional_users[0].password
})
sample_app_startup_script = templatefile("${path.module}/scripts/build/startup_scripts/sample_app/sample_app.sh.tpl", {})
}
resource "random_id" "default" {
count = var.deployment_id == null ? 1 : 0
byte_length = 2
}
data "google_compute_zones" "primary_available_zones" {
project = local.project.project_id
region = var.region
status = "UP"
}
data "google_compute_zones" "secondary_available_zones" {
project = local.project.project_id
region = var.region_secondary
status = "UP"
}
#########################################################################
# WEB HOSTING PROJECT
#########################################################################
data "google_project" "existing_project" {
count = var.create_project ? 0 : 1
project_id = var.project_id_prefix
}
module "project_radlab_web_hosting" {
count = var.create_project ? 1 : 0
source = "terraform-google-modules/project-factory/google"
version = "~> 13.0"
name = format("%s-%s", var.project_id_prefix, local.random_id)
random_project_id = false
folder_id = var.folder_id
billing_account = var.billing_account_id
org_id = var.organization_id
}
resource "google_project_service" "enabled_services" {
for_each = toset(local.project_services)
project = local.project.project_id
service = each.value
disable_dependent_services = true
disable_on_destroy = true
depends_on = [
module.project_radlab_web_hosting
]
}
#########################################################################
# Service Account to connect to Cloud SQL
#########################################################################
resource "google_service_account" "sa_p_cloud_sql" {
project = local.project.project_id
account_id = "gce-sql-sa"
display_name = "Service Account to connect Cloud SQL"
}
#########################################################################
# Creating GCE VMs in vpc-xlb
#########################################################################
# data "google_compute_image" "debian_11_bullseye" {
# family = "debian-11"
# project = "debian-cloud"
# }
resource "google_compute_instance" "web1_vpc_xlb" {
project = local.project.project_id
zone = data.google_compute_zones.primary_available_zones.names.0
name = "web1-vpc-xlb"
machine_type = "f1-micro"
allow_stopping_for_update = true
metadata_startup_script = local.web_startup_script
metadata = {
enable-oslogin = true
}
boot_disk {
initialize_params {
# image = data.google_compute_image.debian_11_bullseye.self_link
image = "debian-cloud/debian-11"
}
}
network_interface {
subnetwork = google_compute_subnetwork.subnetwork_primary.name
subnetwork_project = local.project.project_id
network_ip = cidrhost(google_compute_subnetwork.subnetwork_primary.ip_cidr_range, 2)
}
service_account {
# Google recommends custom service accounts that have cloud-platform scope and permissions granted via IAM Roles.
email = google_service_account.sa_p_cloud_sql.email
scopes = ["cloud-platform"]
}
depends_on = [
time_sleep.wait_120_seconds,
google_compute_router_nat.nat_gw_region_primary
]
}
resource "google_compute_instance" "web2_vpc_xlb" {
project = local.project.project_id
zone = data.google_compute_zones.primary_available_zones.names.1
name = "web2-vpc-xlb"
machine_type = "f1-micro"
allow_stopping_for_update = true
metadata_startup_script = local.web_startup_script
metadata = {
enable-oslogin = true
}
boot_disk {
initialize_params {
# image = data.google_compute_image.debian_11_bullseye.self_link
image = "debian-cloud/debian-11"
}
}
network_interface {
subnetwork = google_compute_subnetwork.subnetwork_primary.name
subnetwork_project = local.project.project_id
network_ip = cidrhost(google_compute_subnetwork.subnetwork_primary.ip_cidr_range, 3)
}
service_account {
# Google recommends custom service accounts that have cloud-platform scope and permissions granted via IAM Roles.
email = google_service_account.sa_p_cloud_sql.email
scopes = ["cloud-platform"]
}
depends_on = [
time_sleep.wait_120_seconds,
google_compute_router_nat.nat_gw_region_primary
]
}
resource "google_compute_instance" "web3_vpc_xlb" {
project = local.project.project_id
zone = data.google_compute_zones.primary_available_zones.names.2
name = "web3-vpc-xlb"
machine_type = "f1-micro"
allow_stopping_for_update = true
metadata_startup_script = local.web_startup_script
metadata = {
enable-oslogin = true
}
boot_disk {
initialize_params {
# image = data.google_compute_image.debian_11_bullseye.self_link
image = "debian-cloud/debian-11"
}
}
network_interface {
subnetwork = google_compute_subnetwork.subnetwork_primary.name
subnetwork_project = local.project.project_id
network_ip = cidrhost(google_compute_subnetwork.subnetwork_primary.ip_cidr_range, 4)
}
service_account {
# Google recommends custom service accounts that have cloud-platform scope and permissions granted via IAM Roles.
email = google_service_account.sa_p_cloud_sql.email
scopes = ["cloud-platform"]
}
depends_on = [
time_sleep.wait_120_seconds,
google_compute_router_nat.nat_gw_region_primary
]
}
resource "google_compute_instance" "web4_vpc_xlb" {
project = local.project.project_id
zone = data.google_compute_zones.secondary_available_zones.names.2
name = "web4-vpc-xlb"
machine_type = "f1-micro"
allow_stopping_for_update = true
metadata_startup_script = local.sample_app_startup_script
metadata = {
enable-oslogin = true
}
boot_disk {
initialize_params {
# image = data.google_compute_image.debian_11_bullseye.self_link
image = "debian-cloud/debian-11"
}
}
network_interface {
subnetwork = google_compute_subnetwork.subnetwork_secondary.name
subnetwork_project = local.project.project_id
network_ip = cidrhost(google_compute_subnetwork.subnetwork_secondary.ip_cidr_range, 2)
}
service_account {
# Google recommends custom service accounts that have cloud-platform scope and permissions granted via IAM Roles.
email = google_service_account.sa_p_cloud_sql.email
scopes = ["cloud-platform"]
}
depends_on = [
time_sleep.wait_120_seconds,
google_compute_router_nat.nat_gw_region_secondary
]
}
#########################################################################
# GCS bucket / Bucket Objects / Bucket Bindings
#########################################################################
resource "google_storage_bucket" "gcs_image_bucket" {
name = join("", ["gcs_image_bucket-", local.project.project_id])
location = var.region
project = local.project.project_id
force_destroy = true
depends_on = [
google_project_service.enabled_services,
time_sleep.wait_120_seconds
]
}
resource "google_storage_object_access_control" "public_rule" {
object = google_storage_bucket_object.picture.name
bucket = google_storage_bucket.gcs_image_bucket.name
role = "READER"
entity = "allUsers"
}
resource "google_storage_bucket_object" "picture" {
name = "images/countryRoad.jpg"
source = "./scripts/build/img/clear-day-brock-mountain-drive_800.jpg"
bucket = google_storage_bucket.gcs_image_bucket.name
}
resource "google_storage_bucket_iam_binding" "binding" {
bucket = google_storage_bucket.gcs_image_bucket.name
role = "roles/storage.admin"
members = toset(concat(formatlist("user:%s", var.trusted_users), formatlist("group:%s", var.trusted_groups)))
}
#########################################################################
# Unmanaged Instance Group - vpc-xlb
#########################################################################
resource "google_compute_instance_group" "ig_us_c1_content_list" {
name = "ig-us-c1-content-list"
description = "Unmanaged instance group created via terraform"
project = local.project.project_id
instances = [
google_compute_instance.web1_vpc_xlb.self_link
]
named_port {
name = "http"
port = "80"
}
zone = data.google_compute_zones.primary_available_zones.names.0
depends_on = [google_compute_instance.web1_vpc_xlb]
}
resource "google_compute_instance_group" "ig_us_c1_content_create" {
name = "ig-us-c1-content-create"
description = "Unmanaged instance group created via terraform"
project = local.project.project_id
instances = [
google_compute_instance.web2_vpc_xlb.self_link
]
named_port {
name = "http"
port = "80"
}
zone = data.google_compute_zones.primary_available_zones.names.1
depends_on = [google_compute_instance.web2_vpc_xlb]
}
resource "google_compute_instance_group" "ig_us_c1_region" {
name = "ig-us-c1-region"
description = "Unmanaged instance group created via terraform"
project = local.project.project_id
instances = [
google_compute_instance.web3_vpc_xlb.self_link
]
named_port {
name = "http"
port = "80"
}
zone = data.google_compute_zones.primary_available_zones.names.2
depends_on = [google_compute_instance.web3_vpc_xlb]
}
resource "google_compute_instance_group" "ig_secondary_region" {
name = "ig-${var.region_secondary}-region"
description = "Unmanaged instance group created via terraform"
project = local.project.project_id
instances = [
google_compute_instance.web4_vpc_xlb.self_link
]
named_port {
name = "http"
port = "80"
}
zone = data.google_compute_zones.secondary_available_zones.names.2
depends_on = [google_compute_instance.web4_vpc_xlb]
}
#########################################################################
# Cloud Armor
#########################################################################
resource "google_compute_security_policy" "policy" {
name = "security-policy"
project = local.project.project_id
rule {
action = "deny(403)"
priority = "1000"
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["9.9.9.0/24"]
}
}
description = "Deny access to IPs in 9.9.9.0/24"
}
rule {
action = "allow"
priority = "2147483647"
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["*"]
}
}
description = "default rule"
}
}
#########################################################################
# Cloud CDN with GCS bucket
#########################################################################
resource "google_compute_backend_bucket" "be_http_cdn_gcs" {
name = "be-http-cdn-gcs"
description = "Contains test images"
project = local.project.project_id
bucket_name = google_storage_bucket.gcs_image_bucket.name
enable_cdn = true
}
#########################################################################
# L7 HTTP Load Balancer - Health Check
#########################################################################
resource "google_compute_health_check" "http_hc" {
name = "http-hc"
timeout_sec = 1
check_interval_sec = 30
http_health_check {
port = 80
request_path = "/hc"
}
project = local.project.project_id
}
#########################################################################
# Global HTTP Load Balancer - Content Based
#########################################################################
resource "google_compute_backend_service" "be_http_content_based_list" {
name = "be-http-content-based-list"
port_name = "http"
protocol = "HTTP"
project = local.project.project_id
timeout_sec = 10
backend {
group = google_compute_instance_group.ig_us_c1_content_list.self_link
balancing_mode = "UTILIZATION"
max_utilization = 0.8
}
health_checks = [google_compute_health_check.http_hc.id]
}
resource "google_compute_backend_service" "be_http_content_based_create" {
name = "be-http-content-based-create"
port_name = "http"
protocol = "HTTP"
security_policy = google_compute_security_policy.policy.name
project = local.project.project_id
timeout_sec = 10
backend {
group = google_compute_instance_group.ig_us_c1_content_create.self_link
balancing_mode = "UTILIZATION"
max_utilization = 0.8
}
health_checks = [google_compute_health_check.http_hc.id]
}
resource "google_compute_url_map" "http_lb_content_based" {
name = "http-lb-content-based"
description = "L7 Global load balancer Content based"
project = local.project.project_id
default_service = google_compute_backend_service.be_http_content_based_list.id
host_rule {
hosts = ["*"]
path_matcher = "allpaths"
}
path_matcher {
name = "allpaths"
default_service = google_compute_backend_service.be_http_content_based_list.id
path_rule {
paths = ["/*"]
service = google_compute_backend_service.be_http_content_based_list.id
}
path_rule {
paths = ["/create", "/create/*", "/delete", "/delete/*"]
service = google_compute_backend_service.be_http_content_based_create.id
}
}
}
resource "google_compute_target_http_proxy" "target_proxy_content_based" {
project = local.project.project_id
name = "target-proxy-content-based"
url_map = google_compute_url_map.http_lb_content_based.self_link
}
resource "google_compute_global_forwarding_rule" "fe_http_content_based" {
name = "fe-http-content-based"
target = google_compute_target_http_proxy.target_proxy_content_based.self_link
port_range = "80"
project = local.project.project_id
}
#########################################################################
# Global HTTP Load Balancer - Cross Region
#########################################################################
resource "google_compute_backend_service" "be_http_cross_region" {
name = "be-http-cross-region"
port_name = "http"
protocol = "HTTP"
project = local.project.project_id
timeout_sec = 10
backend {
group = google_compute_instance_group.ig_us_c1_region.self_link
balancing_mode = "UTILIZATION"
max_utilization = 0.8
}
backend {
group = google_compute_instance_group.ig_secondary_region.self_link
}
health_checks = [google_compute_health_check.http_hc.id]
}
resource "google_compute_url_map" "http_lb_cross_region" {
name = "http-lb-cross-region"
description = "L7 Global load balancer Cross Region"
project = local.project.project_id
default_service = google_compute_backend_service.be_http_cross_region.id
host_rule {
hosts = ["*"]
path_matcher = "allpaths"
}
path_matcher {
name = "allpaths"
default_service = google_compute_backend_service.be_http_cross_region.id
path_rule {
paths = ["/*"]
service = google_compute_backend_service.be_http_cross_region.id
}
path_rule {
paths = ["/images/*"]
service = google_compute_backend_bucket.be_http_cdn_gcs.id
}
}
}
resource "google_compute_target_http_proxy" "target_proxy_cross_region" {
project = local.project.project_id
name = "target-proxy-cross-region"
url_map = google_compute_url_map.http_lb_cross_region.self_link
}
resource "google_compute_global_forwarding_rule" "fe_http_cross_region_cdn" {
name = "fe-http-cross-region-cdn"
target = google_compute_target_http_proxy.target_proxy_cross_region.self_link
port_range = "80"
project = local.project.project_id
}