Pablo Alejandro Costesich's Blog

Infrastructure as Code

Posted by @pcostesi on 2021-07-14Time to read: 94 minutes

In the last post we went over the main setup of this blog. However, we can go the extra mile. If we ever want to deploy another instance of this site (or any other), we would have to repeat the process, potentially making a mistake. Plus, it is a great chance to learn something!

Enter Terraform, an Infrastructure as Code tool that allows us to define our resources declaratively. This is a repeatable and safe way to deploy our pipeline and build the resources we need to get the site running.

A Brief Introduction to the Nomenclature

First, resources are anything that can be created, managed, and destroyed by the tool for a given service provider. They can be VMs, storage, DNS entries, or even accounts. Resources may be customized through inputs, which are unbound variables set during execution from several sources, often unversioned.

Then we have providers, which are bindings through which Terraform will understand those resources and how to work with them.

As a result of the interactions with the providers and resources, a snapshot of the infrastructure is stored in the state, which can be stored locally and added to the version control system, or remotely in a centralized storage.

The state is populated by the outputs, which keep crucial information in variables for later use. These can be used as inputs to other resources, too.

Terraform also has modules, which we will not cover in this post, but are a way to re-use configuration files.

Terraform performs its job in several stages:

  • Init: will load all the providers to talk to the services specified in the configuration files.
  • Plan: will perform an initial assessment of the infrastructure and describe the actions needed to achieve the desired state.
  • Apply: will perform these actions.
  • Destroy: will discard the resources when they are no longer needed.

Basic Anatomy of a Terraform Project

As Terraform uses a declarative approach, there is no "blessed" main file that will be loaded first. However, it is conventional to use main.tf as a place to set up providers, variables.tf to declare variables, and *.auto.tfvars to load sensitive information that should not be under version control. The rest is usually free-form.

This is how I initially set up the project:

~/s/i/t/blog ❯❯❯ tree .
.
├── build.tf
├── dns.tf
├── main.tf
├── monitoring.tf
├── routing.tf
├── secret.auto.tfvars
├── storage.tf
└── variables.tf

0 directories, 8 files
  • build.tf contains the Google Cloud Build trigger definition.
  • dns.tf contains the DNS records and zones.
  • main.tf contains the providers and some other config.
  • monitoring.tf has some handy alerts regarding certificates and uptime.
  • routing.tf describes the frontend and backend for the load balancer, and sets up things like CloudCDN.
  • storage.tf creates and sets up the bucket with the relevant policies.
  • variables.tf and secret.auto.tfvars hold variables and their values.

A Breakdown of the Pipeline

Before We Continue

Make sure to create a service account with limited access to the resources we are going to create, and to enable the Google Cloud Build app in GitHub, as well as registering the relevant service account in the Google Search Console and to follow the procedures needed for verifying the site.

Main

We need to add both google and google-beta to access some functionality in Google Cloud. As you can see, we are storing the state in another bucket, and we are using a service account with limited permissions to set up these resources.

resource "google_cloudbuild_trigger" "site_trigger" {
provider = google-beta
name = "${var.project}-trigger"
project = var.project
github {
push {
branch = "master"
}
owner = "pcostesi"
name = var.static_site
}
filename = "cloudbuild.yaml"
}
view raw build.tf hosted with ❤ by GitHub
steps:
- name: node:current
entrypoint: npm
args: ["install"]
- name: node:current
entrypoint: npm
args: ["run", "build"]
- name: gcr.io/cloud-builders/gsutil
args: ["-m", "rsync", "-r", "-c", "-d", "./public", "gs://pcostesi.dev"]
view raw cloudbuild.yaml hosted with ❤ by GitHub
# Global IP, needed for the load balancer (maybe this should go to `routing.tf`)
resource "google_compute_global_address" "static_sites" {
provider = google
name = "static-sites"
}
# Managed DNS zone
data "google_dns_managed_zone" "main_site" {
provider = google
name = var.project
}
# Bind IP to the DNS
resource "google_dns_record_set" "static_site" {
provider = google
name = "${var.static_site}."
type = "A"
ttl = 300
managed_zone = data.google_dns_managed_zone.main_site.name
rrdatas = [google_compute_global_address.static_sites.address]
}
# Site verification
resource "google_dns_record_set" "static_site_txt" {
provider = google
name = "${var.static_site}."
type = "TXT"
ttl = 300
managed_zone = data.google_dns_managed_zone.main_site.name
rrdatas = ["google-site-verification=${var.site_verification}"]
}
view raw dns.tf hosted with ❤ by GitHub
terraform {
required_providers {
google = {
source = "hashicorp/google"
}
google-beta = {
source = "hashicorp/google-beta"
}
}
backend "gcs"{
bucket = "a-bucket-for-storing-terraform-state"
prefix = "blog"
credentials = "/path/to/your/google/credentials.json"
}
}
provider "google" {
credentials = file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
provider "google-beta" {
credentials = file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
view raw main.tf hosted with ❤ by GitHub
resource "google_monitoring_notification_channel" "basic" {
display_name = "Main Notification Channel"
type = "email"
project = var.project
provider = google-beta
labels = {
email_address = var.email_address
}
}
resource "google_monitoring_uptime_check_config" "static_site_https" {
provider = google-beta
display_name = "${var.project}-https-uptime-check"
timeout = "60s"
project = var.project
http_check {
path = "/"
port = "443"
use_ssl = true
validate_ssl = true
}
monitored_resource {
type = "uptime_url"
labels = {
project_id = var.project
host = var.static_site
}
}
}
resource "google_monitoring_alert_policy" "static_site_alert_policy" {
display_name = "Static Site Alert Policy"
combiner = "OR"
conditions {
display_name = "SSL certificate expiring soon"
condition_threshold {
filter = "metric.type=\"monitoring.googleapis.com/uptime_check/time_until_ssl_cert_expires\" AND resource.type=\"uptime_url\""
duration = "600s"
comparison = "COMPARISON_LT"
threshold_value = 15
trigger {
count = 1
}
aggregations {
alignment_period = "1200s"
per_series_aligner = "ALIGN_NEXT_OLDER"
cross_series_reducer = "REDUCE_MEAN"
group_by_fields = ["resource.label.*"]
}
}
}
user_labels = {
"uptime" = "ssl_cert_expiration"
"version" = "1"
}
notification_channels = [ google_monitoring_notification_channel.basic.name ]
}
view raw monitoring.tf hosted with ❤ by GitHub
# Add the bucket as a CDN backend
resource "google_compute_backend_bucket" "static_site" {
provider = google
name = "${var.project}-backend"
description = "Contains files needed by the website"
bucket_name = google_storage_bucket.static_site.name
enable_cdn = true
depends_on = [google_dns_record_set.static_site_txt]
}
# HTTPS certificate
resource "google_compute_managed_ssl_certificate" "static_site" {
provider = google-beta
name = "${var.project}-cert"
managed {
domains = [google_dns_record_set.static_site.name]
}
}
# GCP URL MAP (frontend config)
resource "google_compute_url_map" "static_site" {
provider = google
name = "${var.project}-url-map"
default_service = google_compute_backend_bucket.static_site.self_link
}
# GCP proxy (load balancer)
resource "google_compute_target_https_proxy" "static_site" {
provider = google
name = "${var.project}-target-proxy"
url_map = google_compute_url_map.static_site.self_link
ssl_certificates = [google_compute_managed_ssl_certificate.static_site.self_link]
}
# GCP forwarding rule (IP <-> Load Balancer mapping)
resource "google_compute_global_forwarding_rule" "default" {
provider = google
name = "${var.project}-forwarding-rule"
load_balancing_scheme = "EXTERNAL"
ip_address = google_compute_global_address.static_sites.address
ip_protocol = "TCP"
port_range = "443"
target = google_compute_target_https_proxy.static_site.self_link
}
view raw routing.tf hosted with ❤ by GitHub
resource "google_storage_bucket" "static_site" {
name = var.static_site
force_destroy = true
uniform_bucket_level_access = false
website {
main_page_suffix = "index.html"
not_found_page = "404.html"
}
cors {
origin = ["https://${var.static_site}"]
method = ["GET", "HEAD", "PUT", "POST", "DELETE"]
response_header = ["*"]
max_age_seconds = 3600
}
}
resource "google_storage_default_object_acl" "static_site_read" {
bucket = google_storage_bucket.static_site.name
role_entity = ["READER:allUsers"]
}
view raw storage.tf hosted with ❤ by GitHub
variable "project" {
default = "pcostesi-dev"
}
variable "credentials" {
default = "../credentials.json"
}
variable "region" {
default = "us-central1"
}
variable "zone" {
default = "us-central1-c"
}
variable "name" {
default = "main"
}
variable "static_site" {
default = "pcostesi.dev"
}
variable "email_address" {}
variable "site_verification" {}
view raw variables.tf hosted with ❤ by GitHub

Variables

These are the placeholders we are going to override or set up on future builds.

resource "google_cloudbuild_trigger" "site_trigger" {
provider = google-beta
name = "${var.project}-trigger"
project = var.project
github {
push {
branch = "master"
}
owner = "pcostesi"
name = var.static_site
}
filename = "cloudbuild.yaml"
}
view raw build.tf hosted with ❤ by GitHub
steps:
- name: node:current
entrypoint: npm
args: ["install"]
- name: node:current
entrypoint: npm
args: ["run", "build"]
- name: gcr.io/cloud-builders/gsutil
args: ["-m", "rsync", "-r", "-c", "-d", "./public", "gs://pcostesi.dev"]
view raw cloudbuild.yaml hosted with ❤ by GitHub
# Global IP, needed for the load balancer (maybe this should go to `routing.tf`)
resource "google_compute_global_address" "static_sites" {
provider = google
name = "static-sites"
}
# Managed DNS zone
data "google_dns_managed_zone" "main_site" {
provider = google
name = var.project
}
# Bind IP to the DNS
resource "google_dns_record_set" "static_site" {
provider = google
name = "${var.static_site}."
type = "A"
ttl = 300
managed_zone = data.google_dns_managed_zone.main_site.name
rrdatas = [google_compute_global_address.static_sites.address]
}
# Site verification
resource "google_dns_record_set" "static_site_txt" {
provider = google
name = "${var.static_site}."
type = "TXT"
ttl = 300
managed_zone = data.google_dns_managed_zone.main_site.name
rrdatas = ["google-site-verification=${var.site_verification}"]
}
view raw dns.tf hosted with ❤ by GitHub
terraform {
required_providers {
google = {
source = "hashicorp/google"
}
google-beta = {
source = "hashicorp/google-beta"
}
}
backend "gcs"{
bucket = "a-bucket-for-storing-terraform-state"
prefix = "blog"
credentials = "/path/to/your/google/credentials.json"
}
}
provider "google" {
credentials = file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
provider "google-beta" {
credentials = file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
view raw main.tf hosted with ❤ by GitHub
resource "google_monitoring_notification_channel" "basic" {
display_name = "Main Notification Channel"
type = "email"
project = var.project
provider = google-beta
labels = {
email_address = var.email_address
}
}
resource "google_monitoring_uptime_check_config" "static_site_https" {
provider = google-beta
display_name = "${var.project}-https-uptime-check"
timeout = "60s"
project = var.project
http_check {
path = "/"
port = "443"
use_ssl = true
validate_ssl = true
}
monitored_resource {
type = "uptime_url"
labels = {
project_id = var.project
host = var.static_site
}
}
}
resource "google_monitoring_alert_policy" "static_site_alert_policy" {
display_name = "Static Site Alert Policy"
combiner = "OR"
conditions {
display_name = "SSL certificate expiring soon"
condition_threshold {
filter = "metric.type=\"monitoring.googleapis.com/uptime_check/time_until_ssl_cert_expires\" AND resource.type=\"uptime_url\""
duration = "600s"
comparison = "COMPARISON_LT"
threshold_value = 15
trigger {
count = 1
}
aggregations {
alignment_period = "1200s"
per_series_aligner = "ALIGN_NEXT_OLDER"
cross_series_reducer = "REDUCE_MEAN"
group_by_fields = ["resource.label.*"]
}
}
}
user_labels = {
"uptime" = "ssl_cert_expiration"
"version" = "1"
}
notification_channels = [ google_monitoring_notification_channel.basic.name ]
}
view raw monitoring.tf hosted with ❤ by GitHub
# Add the bucket as a CDN backend
resource "google_compute_backend_bucket" "static_site" {
provider = google
name = "${var.project}-backend"
description = "Contains files needed by the website"
bucket_name = google_storage_bucket.static_site.name
enable_cdn = true
depends_on = [google_dns_record_set.static_site_txt]
}
# HTTPS certificate
resource "google_compute_managed_ssl_certificate" "static_site" {
provider = google-beta
name = "${var.project}-cert"
managed {
domains = [google_dns_record_set.static_site.name]
}
}
# GCP URL MAP (frontend config)
resource "google_compute_url_map" "static_site" {
provider = google
name = "${var.project}-url-map"
default_service = google_compute_backend_bucket.static_site.self_link
}
# GCP proxy (load balancer)
resource "google_compute_target_https_proxy" "static_site" {
provider = google
name = "${var.project}-target-proxy"
url_map = google_compute_url_map.static_site.self_link
ssl_certificates = [google_compute_managed_ssl_certificate.static_site.self_link]
}
# GCP forwarding rule (IP <-> Load Balancer mapping)
resource "google_compute_global_forwarding_rule" "default" {
provider = google
name = "${var.project}-forwarding-rule"
load_balancing_scheme = "EXTERNAL"
ip_address = google_compute_global_address.static_sites.address
ip_protocol = "TCP"
port_range = "443"
target = google_compute_target_https_proxy.static_site.self_link
}
view raw routing.tf hosted with ❤ by GitHub
resource "google_storage_bucket" "static_site" {
name = var.static_site
force_destroy = true
uniform_bucket_level_access = false
website {
main_page_suffix = "index.html"
not_found_page = "404.html"
}
cors {
origin = ["https://${var.static_site}"]
method = ["GET", "HEAD", "PUT", "POST", "DELETE"]
response_header = ["*"]
max_age_seconds = 3600
}
}
resource "google_storage_default_object_acl" "static_site_read" {
bucket = google_storage_bucket.static_site.name
role_entity = ["READER:allUsers"]
}
view raw storage.tf hosted with ❤ by GitHub
variable "project" {
default = "pcostesi-dev"
}
variable "credentials" {
default = "../credentials.json"
}
variable "region" {
default = "us-central1"
}
variable "zone" {
default = "us-central1-c"
}
variable "name" {
default = "main"
}
variable "static_site" {
default = "pcostesi.dev"
}
variable "email_address" {}
variable "site_verification" {}
view raw variables.tf hosted with ❤ by GitHub

Storage

We created a bucket with public read policy so the load balancer could fetch the files. We now have to make this process repeatable:

resource "google_cloudbuild_trigger" "site_trigger" {
provider = google-beta
name = "${var.project}-trigger"
project = var.project
github {
push {
branch = "master"
}
owner = "pcostesi"
name = var.static_site
}
filename = "cloudbuild.yaml"
}
view raw build.tf hosted with ❤ by GitHub
steps:
- name: node:current
entrypoint: npm
args: ["install"]
- name: node:current
entrypoint: npm
args: ["run", "build"]
- name: gcr.io/cloud-builders/gsutil
args: ["-m", "rsync", "-r", "-c", "-d", "./public", "gs://pcostesi.dev"]
view raw cloudbuild.yaml hosted with ❤ by GitHub
# Global IP, needed for the load balancer (maybe this should go to `routing.tf`)
resource "google_compute_global_address" "static_sites" {
provider = google
name = "static-sites"
}
# Managed DNS zone
data "google_dns_managed_zone" "main_site" {
provider = google
name = var.project
}
# Bind IP to the DNS
resource "google_dns_record_set" "static_site" {
provider = google
name = "${var.static_site}."
type = "A"
ttl = 300
managed_zone = data.google_dns_managed_zone.main_site.name
rrdatas = [google_compute_global_address.static_sites.address]
}
# Site verification
resource "google_dns_record_set" "static_site_txt" {
provider = google
name = "${var.static_site}."
type = "TXT"
ttl = 300
managed_zone = data.google_dns_managed_zone.main_site.name
rrdatas = ["google-site-verification=${var.site_verification}"]
}
view raw dns.tf hosted with ❤ by GitHub
terraform {
required_providers {
google = {
source = "hashicorp/google"
}
google-beta = {
source = "hashicorp/google-beta"
}
}
backend "gcs"{
bucket = "a-bucket-for-storing-terraform-state"
prefix = "blog"
credentials = "/path/to/your/google/credentials.json"
}
}
provider "google" {
credentials = file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
provider "google-beta" {
credentials = file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
view raw main.tf hosted with ❤ by GitHub
resource "google_monitoring_notification_channel" "basic" {
display_name = "Main Notification Channel"
type = "email"
project = var.project
provider = google-beta
labels = {
email_address = var.email_address
}
}
resource "google_monitoring_uptime_check_config" "static_site_https" {
provider = google-beta
display_name = "${var.project}-https-uptime-check"
timeout = "60s"
project = var.project
http_check {
path = "/"
port = "443"
use_ssl = true
validate_ssl = true
}
monitored_resource {
type = "uptime_url"
labels = {
project_id = var.project
host = var.static_site
}
}
}
resource "google_monitoring_alert_policy" "static_site_alert_policy" {
display_name = "Static Site Alert Policy"
combiner = "OR"
conditions {
display_name = "SSL certificate expiring soon"
condition_threshold {
filter = "metric.type=\"monitoring.googleapis.com/uptime_check/time_until_ssl_cert_expires\" AND resource.type=\"uptime_url\""
duration = "600s"
comparison = "COMPARISON_LT"
threshold_value = 15
trigger {
count = 1
}
aggregations {
alignment_period = "1200s"
per_series_aligner = "ALIGN_NEXT_OLDER"
cross_series_reducer = "REDUCE_MEAN"
group_by_fields = ["resource.label.*"]
}
}
}
user_labels = {
"uptime" = "ssl_cert_expiration"
"version" = "1"
}
notification_channels = [ google_monitoring_notification_channel.basic.name ]
}
view raw monitoring.tf hosted with ❤ by GitHub
# Add the bucket as a CDN backend
resource "google_compute_backend_bucket" "static_site" {
provider = google
name = "${var.project}-backend"
description = "Contains files needed by the website"
bucket_name = google_storage_bucket.static_site.name
enable_cdn = true
depends_on = [google_dns_record_set.static_site_txt]
}
# HTTPS certificate
resource "google_compute_managed_ssl_certificate" "static_site" {
provider = google-beta
name = "${var.project}-cert"
managed {
domains = [google_dns_record_set.static_site.name]
}
}
# GCP URL MAP (frontend config)
resource "google_compute_url_map" "static_site" {
provider = google
name = "${var.project}-url-map"
default_service = google_compute_backend_bucket.static_site.self_link
}
# GCP proxy (load balancer)
resource "google_compute_target_https_proxy" "static_site" {
provider = google
name = "${var.project}-target-proxy"
url_map = google_compute_url_map.static_site.self_link
ssl_certificates = [google_compute_managed_ssl_certificate.static_site.self_link]
}
# GCP forwarding rule (IP <-> Load Balancer mapping)
resource "google_compute_global_forwarding_rule" "default" {
provider = google
name = "${var.project}-forwarding-rule"
load_balancing_scheme = "EXTERNAL"
ip_address = google_compute_global_address.static_sites.address
ip_protocol = "TCP"
port_range = "443"
target = google_compute_target_https_proxy.static_site.self_link
}
view raw routing.tf hosted with ❤ by GitHub
resource "google_storage_bucket" "static_site" {
name = var.static_site
force_destroy = true
uniform_bucket_level_access = false
website {
main_page_suffix = "index.html"
not_found_page = "404.html"
}
cors {
origin = ["https://${var.static_site}"]
method = ["GET", "HEAD", "PUT", "POST", "DELETE"]
response_header = ["*"]
max_age_seconds = 3600
}
}
resource "google_storage_default_object_acl" "static_site_read" {
bucket = google_storage_bucket.static_site.name
role_entity = ["READER:allUsers"]
}
view raw storage.tf hosted with ❤ by GitHub
variable "project" {
default = "pcostesi-dev"
}
variable "credentials" {
default = "../credentials.json"
}
variable "region" {
default = "us-central1"
}
variable "zone" {
default = "us-central1-c"
}
variable "name" {
default = "main"
}
variable "static_site" {
default = "pcostesi.dev"
}
variable "email_address" {}
variable "site_verification" {}
view raw variables.tf hosted with ❤ by GitHub

CI/CD Pipeline

The pipeline is split in two. First, the trigger, that points to the cloudbuild.yaml file:

resource "google_cloudbuild_trigger" "site_trigger" {
provider = google-beta
name = "${var.project}-trigger"
project = var.project
github {
push {
branch = "master"
}
owner = "pcostesi"
name = var.static_site
}
filename = "cloudbuild.yaml"
}
view raw build.tf hosted with ❤ by GitHub
steps:
- name: node:current
entrypoint: npm
args: ["install"]
- name: node:current
entrypoint: npm
args: ["run", "build"]
- name: gcr.io/cloud-builders/gsutil
args: ["-m", "rsync", "-r", "-c", "-d", "./public", "gs://pcostesi.dev"]
view raw cloudbuild.yaml hosted with ❤ by GitHub
# Global IP, needed for the load balancer (maybe this should go to `routing.tf`)
resource "google_compute_global_address" "static_sites" {
provider = google
name = "static-sites"
}
# Managed DNS zone
data "google_dns_managed_zone" "main_site" {
provider = google
name = var.project
}
# Bind IP to the DNS
resource "google_dns_record_set" "static_site" {
provider = google
name = "${var.static_site}."
type = "A"
ttl = 300
managed_zone = data.google_dns_managed_zone.main_site.name
rrdatas = [google_compute_global_address.static_sites.address]
}
# Site verification
resource "google_dns_record_set" "static_site_txt" {
provider = google
name = "${var.static_site}."
type = "TXT"
ttl = 300
managed_zone = data.google_dns_managed_zone.main_site.name
rrdatas = ["google-site-verification=${var.site_verification}"]
}
view raw dns.tf hosted with ❤ by GitHub
terraform {
required_providers {
google = {
source = "hashicorp/google"
}
google-beta = {
source = "hashicorp/google-beta"
}
}
backend "gcs"{
bucket = "a-bucket-for-storing-terraform-state"
prefix = "blog"
credentials = "/path/to/your/google/credentials.json"
}
}
provider "google" {
credentials = file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
provider "google-beta" {
credentials = file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
view raw main.tf hosted with ❤ by GitHub
resource "google_monitoring_notification_channel" "basic" {
display_name = "Main Notification Channel"
type = "email"
project = var.project
provider = google-beta
labels = {
email_address = var.email_address
}
}
resource "google_monitoring_uptime_check_config" "static_site_https" {
provider = google-beta
display_name = "${var.project}-https-uptime-check"
timeout = "60s"
project = var.project
http_check {
path = "/"
port = "443"
use_ssl = true
validate_ssl = true
}
monitored_resource {
type = "uptime_url"
labels = {
project_id = var.project
host = var.static_site
}
}
}
resource "google_monitoring_alert_policy" "static_site_alert_policy" {
display_name = "Static Site Alert Policy"
combiner = "OR"
conditions {
display_name = "SSL certificate expiring soon"
condition_threshold {
filter = "metric.type=\"monitoring.googleapis.com/uptime_check/time_until_ssl_cert_expires\" AND resource.type=\"uptime_url\""
duration = "600s"
comparison = "COMPARISON_LT"
threshold_value = 15
trigger {
count = 1
}
aggregations {
alignment_period = "1200s"
per_series_aligner = "ALIGN_NEXT_OLDER"
cross_series_reducer = "REDUCE_MEAN"
group_by_fields = ["resource.label.*"]
}
}
}
user_labels = {
"uptime" = "ssl_cert_expiration"
"version" = "1"
}
notification_channels = [ google_monitoring_notification_channel.basic.name ]
}
view raw monitoring.tf hosted with ❤ by GitHub
# Add the bucket as a CDN backend
resource "google_compute_backend_bucket" "static_site" {
provider = google
name = "${var.project}-backend"
description = "Contains files needed by the website"
bucket_name = google_storage_bucket.static_site.name
enable_cdn = true
depends_on = [google_dns_record_set.static_site_txt]
}
# HTTPS certificate
resource "google_compute_managed_ssl_certificate" "static_site" {
provider = google-beta
name = "${var.project}-cert"
managed {
domains = [google_dns_record_set.static_site.name]
}
}
# GCP URL MAP (frontend config)
resource "google_compute_url_map" "static_site" {
provider = google
name = "${var.project}-url-map"
default_service = google_compute_backend_bucket.static_site.self_link
}
# GCP proxy (load balancer)
resource "google_compute_target_https_proxy" "static_site" {
provider = google
name = "${var.project}-target-proxy"
url_map = google_compute_url_map.static_site.self_link
ssl_certificates = [google_compute_managed_ssl_certificate.static_site.self_link]
}
# GCP forwarding rule (IP <-> Load Balancer mapping)
resource "google_compute_global_forwarding_rule" "default" {
provider = google
name = "${var.project}-forwarding-rule"
load_balancing_scheme = "EXTERNAL"
ip_address = google_compute_global_address.static_sites.address
ip_protocol = "TCP"
port_range = "443"
target = google_compute_target_https_proxy.static_site.self_link
}
view raw routing.tf hosted with ❤ by GitHub
resource "google_storage_bucket" "static_site" {
name = var.static_site
force_destroy = true
uniform_bucket_level_access = false
website {
main_page_suffix = "index.html"
not_found_page = "404.html"
}
cors {
origin = ["https://${var.static_site}"]
method = ["GET", "HEAD", "PUT", "POST", "DELETE"]
response_header = ["*"]
max_age_seconds = 3600
}
}
resource "google_storage_default_object_acl" "static_site_read" {
bucket = google_storage_bucket.static_site.name
role_entity = ["READER:allUsers"]
}
view raw storage.tf hosted with ❤ by GitHub
variable "project" {
default = "pcostesi-dev"
}
variable "credentials" {
default = "../credentials.json"
}
variable "region" {
default = "us-central1"
}
variable "zone" {
default = "us-central1-c"
}
variable "name" {
default = "main"
}
variable "static_site" {
default = "pcostesi.dev"
}
variable "email_address" {}
variable "site_verification" {}
view raw variables.tf hosted with ❤ by GitHub

Then, the cloudbuild.yaml file proper, which is the one we saw in the last post:

resource "google_cloudbuild_trigger" "site_trigger" {
provider = google-beta
name = "${var.project}-trigger"
project = var.project
github {
push {
branch = "master"
}
owner = "pcostesi"
name = var.static_site
}
filename = "cloudbuild.yaml"
}
view raw build.tf hosted with ❤ by GitHub
steps:
- name: node:current
entrypoint: npm
args: ["install"]
- name: node:current
entrypoint: npm
args: ["run", "build"]
- name: gcr.io/cloud-builders/gsutil
args: ["-m", "rsync", "-r", "-c", "-d", "./public", "gs://pcostesi.dev"]
view raw cloudbuild.yaml hosted with ❤ by GitHub
# Global IP, needed for the load balancer (maybe this should go to `routing.tf`)
resource "google_compute_global_address" "static_sites" {
provider = google
name = "static-sites"
}
# Managed DNS zone
data "google_dns_managed_zone" "main_site" {
provider = google
name = var.project
}
# Bind IP to the DNS
resource "google_dns_record_set" "static_site" {
provider = google
name = "${var.static_site}."
type = "A"
ttl = 300
managed_zone = data.google_dns_managed_zone.main_site.name
rrdatas = [google_compute_global_address.static_sites.address]
}
# Site verification
resource "google_dns_record_set" "static_site_txt" {
provider = google
name = "${var.static_site}."
type = "TXT"
ttl = 300
managed_zone = data.google_dns_managed_zone.main_site.name
rrdatas = ["google-site-verification=${var.site_verification}"]
}
view raw dns.tf hosted with ❤ by GitHub
terraform {
required_providers {
google = {
source = "hashicorp/google"
}
google-beta = {
source = "hashicorp/google-beta"
}
}
backend "gcs"{
bucket = "a-bucket-for-storing-terraform-state"
prefix = "blog"
credentials = "/path/to/your/google/credentials.json"
}
}
provider "google" {
credentials = file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
provider "google-beta" {
credentials = file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
view raw main.tf hosted with ❤ by GitHub
resource "google_monitoring_notification_channel" "basic" {
display_name = "Main Notification Channel"
type = "email"
project = var.project
provider = google-beta
labels = {
email_address = var.email_address
}
}
resource "google_monitoring_uptime_check_config" "static_site_https" {
provider = google-beta
display_name = "${var.project}-https-uptime-check"
timeout = "60s"
project = var.project
http_check {
path = "/"
port = "443"
use_ssl = true
validate_ssl = true
}
monitored_resource {
type = "uptime_url"
labels = {
project_id = var.project
host = var.static_site
}
}
}
resource "google_monitoring_alert_policy" "static_site_alert_policy" {
display_name = "Static Site Alert Policy"
combiner = "OR"
conditions {
display_name = "SSL certificate expiring soon"
condition_threshold {
filter = "metric.type=\"monitoring.googleapis.com/uptime_check/time_until_ssl_cert_expires\" AND resource.type=\"uptime_url\""
duration = "600s"
comparison = "COMPARISON_LT"
threshold_value = 15
trigger {
count = 1
}
aggregations {
alignment_period = "1200s"
per_series_aligner = "ALIGN_NEXT_OLDER"
cross_series_reducer = "REDUCE_MEAN"
group_by_fields = ["resource.label.*"]
}
}
}
user_labels = {
"uptime" = "ssl_cert_expiration"
"version" = "1"
}
notification_channels = [ google_monitoring_notification_channel.basic.name ]
}
view raw monitoring.tf hosted with ❤ by GitHub
# Add the bucket as a CDN backend
resource "google_compute_backend_bucket" "static_site" {
provider = google
name = "${var.project}-backend"
description = "Contains files needed by the website"
bucket_name = google_storage_bucket.static_site.name
enable_cdn = true
depends_on = [google_dns_record_set.static_site_txt]
}
# HTTPS certificate
resource "google_compute_managed_ssl_certificate" "static_site" {
provider = google-beta
name = "${var.project}-cert"
managed {
domains = [google_dns_record_set.static_site.name]
}
}
# GCP URL MAP (frontend config)
resource "google_compute_url_map" "static_site" {
provider = google
name = "${var.project}-url-map"
default_service = google_compute_backend_bucket.static_site.self_link
}
# GCP proxy (load balancer)
resource "google_compute_target_https_proxy" "static_site" {
provider = google
name = "${var.project}-target-proxy"
url_map = google_compute_url_map.static_site.self_link
ssl_certificates = [google_compute_managed_ssl_certificate.static_site.self_link]
}
# GCP forwarding rule (IP <-> Load Balancer mapping)
resource "google_compute_global_forwarding_rule" "default" {
provider = google
name = "${var.project}-forwarding-rule"
load_balancing_scheme = "EXTERNAL"
ip_address = google_compute_global_address.static_sites.address
ip_protocol = "TCP"
port_range = "443"
target = google_compute_target_https_proxy.static_site.self_link
}
view raw routing.tf hosted with ❤ by GitHub
resource "google_storage_bucket" "static_site" {
name = var.static_site
force_destroy = true
uniform_bucket_level_access = false
website {
main_page_suffix = "index.html"
not_found_page = "404.html"
}
cors {
origin = ["https://${var.static_site}"]
method = ["GET", "HEAD", "PUT", "POST", "DELETE"]
response_header = ["*"]
max_age_seconds = 3600
}
}
resource "google_storage_default_object_acl" "static_site_read" {
bucket = google_storage_bucket.static_site.name
role_entity = ["READER:allUsers"]
}
view raw storage.tf hosted with ❤ by GitHub
variable "project" {
default = "pcostesi-dev"
}
variable "credentials" {
default = "../credentials.json"
}
variable "region" {
default = "us-central1"
}
variable "zone" {
default = "us-central1-c"
}
variable "name" {
default = "main"
}
variable "static_site" {
default = "pcostesi.dev"
}
variable "email_address" {}
variable "site_verification" {}
view raw variables.tf hosted with ❤ by GitHub

Serving the site

We need to set up the domain name, IP, verify our ownership of the bucket, and point the A record.

resource "google_cloudbuild_trigger" "site_trigger" {
provider = google-beta
name = "${var.project}-trigger"
project = var.project
github {
push {
branch = "master"
}
owner = "pcostesi"
name = var.static_site
}
filename = "cloudbuild.yaml"
}
view raw build.tf hosted with ❤ by GitHub
steps:
- name: node:current
entrypoint: npm
args: ["install"]
- name: node:current
entrypoint: npm
args: ["run", "build"]
- name: gcr.io/cloud-builders/gsutil
args: ["-m", "rsync", "-r", "-c", "-d", "./public", "gs://pcostesi.dev"]
view raw cloudbuild.yaml hosted with ❤ by GitHub
# Global IP, needed for the load balancer (maybe this should go to `routing.tf`)
resource "google_compute_global_address" "static_sites" {
provider = google
name = "static-sites"
}
# Managed DNS zone
data "google_dns_managed_zone" "main_site" {
provider = google
name = var.project
}
# Bind IP to the DNS
resource "google_dns_record_set" "static_site" {
provider = google
name = "${var.static_site}."
type = "A"
ttl = 300
managed_zone = data.google_dns_managed_zone.main_site.name
rrdatas = [google_compute_global_address.static_sites.address]
}
# Site verification
resource "google_dns_record_set" "static_site_txt" {
provider = google
name = "${var.static_site}."
type = "TXT"
ttl = 300
managed_zone = data.google_dns_managed_zone.main_site.name
rrdatas = ["google-site-verification=${var.site_verification}"]
}
view raw dns.tf hosted with ❤ by GitHub
terraform {
required_providers {
google = {
source = "hashicorp/google"
}
google-beta = {
source = "hashicorp/google-beta"
}
}
backend "gcs"{
bucket = "a-bucket-for-storing-terraform-state"
prefix = "blog"
credentials = "/path/to/your/google/credentials.json"
}
}
provider "google" {
credentials = file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
provider "google-beta" {
credentials = file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
view raw main.tf hosted with ❤ by GitHub
resource "google_monitoring_notification_channel" "basic" {
display_name = "Main Notification Channel"
type = "email"
project = var.project
provider = google-beta
labels = {
email_address = var.email_address
}
}
resource "google_monitoring_uptime_check_config" "static_site_https" {
provider = google-beta
display_name = "${var.project}-https-uptime-check"
timeout = "60s"
project = var.project
http_check {
path = "/"
port = "443"
use_ssl = true
validate_ssl = true
}
monitored_resource {
type = "uptime_url"
labels = {
project_id = var.project
host = var.static_site
}
}
}
resource "google_monitoring_alert_policy" "static_site_alert_policy" {
display_name = "Static Site Alert Policy"
combiner = "OR"
conditions {
display_name = "SSL certificate expiring soon"
condition_threshold {
filter = "metric.type=\"monitoring.googleapis.com/uptime_check/time_until_ssl_cert_expires\" AND resource.type=\"uptime_url\""
duration = "600s"
comparison = "COMPARISON_LT"
threshold_value = 15
trigger {
count = 1
}
aggregations {
alignment_period = "1200s"
per_series_aligner = "ALIGN_NEXT_OLDER"
cross_series_reducer = "REDUCE_MEAN"
group_by_fields = ["resource.label.*"]
}
}
}
user_labels = {
"uptime" = "ssl_cert_expiration"
"version" = "1"
}
notification_channels = [ google_monitoring_notification_channel.basic.name ]
}
view raw monitoring.tf hosted with ❤ by GitHub
# Add the bucket as a CDN backend
resource "google_compute_backend_bucket" "static_site" {
provider = google
name = "${var.project}-backend"
description = "Contains files needed by the website"
bucket_name = google_storage_bucket.static_site.name
enable_cdn = true
depends_on = [google_dns_record_set.static_site_txt]
}
# HTTPS certificate
resource "google_compute_managed_ssl_certificate" "static_site" {
provider = google-beta
name = "${var.project}-cert"
managed {
domains = [google_dns_record_set.static_site.name]
}
}
# GCP URL MAP (frontend config)
resource "google_compute_url_map" "static_site" {
provider = google
name = "${var.project}-url-map"
default_service = google_compute_backend_bucket.static_site.self_link
}
# GCP proxy (load balancer)
resource "google_compute_target_https_proxy" "static_site" {
provider = google
name = "${var.project}-target-proxy"
url_map = google_compute_url_map.static_site.self_link
ssl_certificates = [google_compute_managed_ssl_certificate.static_site.self_link]
}
# GCP forwarding rule (IP <-> Load Balancer mapping)
resource "google_compute_global_forwarding_rule" "default" {
provider = google
name = "${var.project}-forwarding-rule"
load_balancing_scheme = "EXTERNAL"
ip_address = google_compute_global_address.static_sites.address
ip_protocol = "TCP"
port_range = "443"
target = google_compute_target_https_proxy.static_site.self_link
}
view raw routing.tf hosted with ❤ by GitHub
resource "google_storage_bucket" "static_site" {
name = var.static_site
force_destroy = true
uniform_bucket_level_access = false
website {
main_page_suffix = "index.html"
not_found_page = "404.html"
}
cors {
origin = ["https://${var.static_site}"]
method = ["GET", "HEAD", "PUT", "POST", "DELETE"]
response_header = ["*"]
max_age_seconds = 3600
}
}
resource "google_storage_default_object_acl" "static_site_read" {
bucket = google_storage_bucket.static_site.name
role_entity = ["READER:allUsers"]
}
view raw storage.tf hosted with ❤ by GitHub
variable "project" {
default = "pcostesi-dev"
}
variable "credentials" {
default = "../credentials.json"
}
variable "region" {
default = "us-central1"
}
variable "zone" {
default = "us-central1-c"
}
variable "name" {
default = "main"
}
variable "static_site" {
default = "pcostesi.dev"
}
variable "email_address" {}
variable "site_verification" {}
view raw variables.tf hosted with ❤ by GitHub

Note: I set up the site verification id before creating the bucket. This is a domain-wide verification id, and the service account creating the bucket is an owner in the search console.

This is the routing as we should have set it up in the last post:

resource "google_cloudbuild_trigger" "site_trigger" {
provider = google-beta
name = "${var.project}-trigger"
project = var.project
github {
push {
branch = "master"
}
owner = "pcostesi"
name = var.static_site
}
filename = "cloudbuild.yaml"
}
view raw build.tf hosted with ❤ by GitHub
steps:
- name: node:current
entrypoint: npm
args: ["install"]
- name: node:current
entrypoint: npm
args: ["run", "build"]
- name: gcr.io/cloud-builders/gsutil
args: ["-m", "rsync", "-r", "-c", "-d", "./public", "gs://pcostesi.dev"]
view raw cloudbuild.yaml hosted with ❤ by GitHub
# Global IP, needed for the load balancer (maybe this should go to `routing.tf`)
resource "google_compute_global_address" "static_sites" {
provider = google
name = "static-sites"
}
# Managed DNS zone
data "google_dns_managed_zone" "main_site" {
provider = google
name = var.project
}
# Bind IP to the DNS
resource "google_dns_record_set" "static_site" {
provider = google
name = "${var.static_site}."
type = "A"
ttl = 300
managed_zone = data.google_dns_managed_zone.main_site.name
rrdatas = [google_compute_global_address.static_sites.address]
}
# Site verification
resource "google_dns_record_set" "static_site_txt" {
provider = google
name = "${var.static_site}."
type = "TXT"
ttl = 300
managed_zone = data.google_dns_managed_zone.main_site.name
rrdatas = ["google-site-verification=${var.site_verification}"]
}
view raw dns.tf hosted with ❤ by GitHub
terraform {
required_providers {
google = {
source = "hashicorp/google"
}
google-beta = {
source = "hashicorp/google-beta"
}
}
backend "gcs"{
bucket = "a-bucket-for-storing-terraform-state"
prefix = "blog"
credentials = "/path/to/your/google/credentials.json"
}
}
provider "google" {
credentials = file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
provider "google-beta" {
credentials = file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
view raw main.tf hosted with ❤ by GitHub
resource "google_monitoring_notification_channel" "basic" {
display_name = "Main Notification Channel"
type = "email"
project = var.project
provider = google-beta
labels = {
email_address = var.email_address
}
}
resource "google_monitoring_uptime_check_config" "static_site_https" {
provider = google-beta
display_name = "${var.project}-https-uptime-check"
timeout = "60s"
project = var.project
http_check {
path = "/"
port = "443"
use_ssl = true
validate_ssl = true
}
monitored_resource {
type = "uptime_url"
labels = {
project_id = var.project
host = var.static_site
}
}
}
resource "google_monitoring_alert_policy" "static_site_alert_policy" {
display_name = "Static Site Alert Policy"
combiner = "OR"
conditions {
display_name = "SSL certificate expiring soon"
condition_threshold {
filter = "metric.type=\"monitoring.googleapis.com/uptime_check/time_until_ssl_cert_expires\" AND resource.type=\"uptime_url\""
duration = "600s"
comparison = "COMPARISON_LT"
threshold_value = 15
trigger {
count = 1
}
aggregations {
alignment_period = "1200s"
per_series_aligner = "ALIGN_NEXT_OLDER"
cross_series_reducer = "REDUCE_MEAN"
group_by_fields = ["resource.label.*"]
}
}
}
user_labels = {
"uptime" = "ssl_cert_expiration"
"version" = "1"
}
notification_channels = [ google_monitoring_notification_channel.basic.name ]
}
view raw monitoring.tf hosted with ❤ by GitHub
# Add the bucket as a CDN backend
resource "google_compute_backend_bucket" "static_site" {
provider = google
name = "${var.project}-backend"
description = "Contains files needed by the website"
bucket_name = google_storage_bucket.static_site.name
enable_cdn = true
depends_on = [google_dns_record_set.static_site_txt]
}
# HTTPS certificate
resource "google_compute_managed_ssl_certificate" "static_site" {
provider = google-beta
name = "${var.project}-cert"
managed {
domains = [google_dns_record_set.static_site.name]
}
}
# GCP URL MAP (frontend config)
resource "google_compute_url_map" "static_site" {
provider = google
name = "${var.project}-url-map"
default_service = google_compute_backend_bucket.static_site.self_link
}
# GCP proxy (load balancer)
resource "google_compute_target_https_proxy" "static_site" {
provider = google
name = "${var.project}-target-proxy"
url_map = google_compute_url_map.static_site.self_link
ssl_certificates = [google_compute_managed_ssl_certificate.static_site.self_link]
}
# GCP forwarding rule (IP <-> Load Balancer mapping)
resource "google_compute_global_forwarding_rule" "default" {
provider = google
name = "${var.project}-forwarding-rule"
load_balancing_scheme = "EXTERNAL"
ip_address = google_compute_global_address.static_sites.address
ip_protocol = "TCP"
port_range = "443"
target = google_compute_target_https_proxy.static_site.self_link
}
view raw routing.tf hosted with ❤ by GitHub
resource "google_storage_bucket" "static_site" {
name = var.static_site
force_destroy = true
uniform_bucket_level_access = false
website {
main_page_suffix = "index.html"
not_found_page = "404.html"
}
cors {
origin = ["https://${var.static_site}"]
method = ["GET", "HEAD", "PUT", "POST", "DELETE"]
response_header = ["*"]
max_age_seconds = 3600
}
}
resource "google_storage_default_object_acl" "static_site_read" {
bucket = google_storage_bucket.static_site.name
role_entity = ["READER:allUsers"]
}
view raw storage.tf hosted with ❤ by GitHub
variable "project" {
default = "pcostesi-dev"
}
variable "credentials" {
default = "../credentials.json"
}
variable "region" {
default = "us-central1"
}
variable "zone" {
default = "us-central1-c"
}
variable "name" {
default = "main"
}
variable "static_site" {
default = "pcostesi.dev"
}
variable "email_address" {}
variable "site_verification" {}
view raw variables.tf hosted with ❤ by GitHub

Bonus Track: monitoring

In the past, I have seen some of my sites silently die by high latency and expired certificates, so I decided to give it a try and set up a simple alarm with some metrics:

resource "google_cloudbuild_trigger" "site_trigger" {
provider = google-beta
name = "${var.project}-trigger"
project = var.project
github {
push {
branch = "master"
}
owner = "pcostesi"
name = var.static_site
}
filename = "cloudbuild.yaml"
}
view raw build.tf hosted with ❤ by GitHub
steps:
- name: node:current
entrypoint: npm
args: ["install"]
- name: node:current
entrypoint: npm
args: ["run", "build"]
- name: gcr.io/cloud-builders/gsutil
args: ["-m", "rsync", "-r", "-c", "-d", "./public", "gs://pcostesi.dev"]
view raw cloudbuild.yaml hosted with ❤ by GitHub
# Global IP, needed for the load balancer (maybe this should go to `routing.tf`)
resource "google_compute_global_address" "static_sites" {
provider = google
name = "static-sites"
}
# Managed DNS zone
data "google_dns_managed_zone" "main_site" {
provider = google
name = var.project
}
# Bind IP to the DNS
resource "google_dns_record_set" "static_site" {
provider = google
name = "${var.static_site}."
type = "A"
ttl = 300
managed_zone = data.google_dns_managed_zone.main_site.name
rrdatas = [google_compute_global_address.static_sites.address]
}
# Site verification
resource "google_dns_record_set" "static_site_txt" {
provider = google
name = "${var.static_site}."
type = "TXT"
ttl = 300
managed_zone = data.google_dns_managed_zone.main_site.name
rrdatas = ["google-site-verification=${var.site_verification}"]
}
view raw dns.tf hosted with ❤ by GitHub
terraform {
required_providers {
google = {
source = "hashicorp/google"
}
google-beta = {
source = "hashicorp/google-beta"
}
}
backend "gcs"{
bucket = "a-bucket-for-storing-terraform-state"
prefix = "blog"
credentials = "/path/to/your/google/credentials.json"
}
}
provider "google" {
credentials = file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
provider "google-beta" {
credentials = file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
view raw main.tf hosted with ❤ by GitHub
resource "google_monitoring_notification_channel" "basic" {
display_name = "Main Notification Channel"
type = "email"
project = var.project
provider = google-beta
labels = {
email_address = var.email_address
}
}
resource "google_monitoring_uptime_check_config" "static_site_https" {
provider = google-beta
display_name = "${var.project}-https-uptime-check"
timeout = "60s"
project = var.project
http_check {
path = "/"
port = "443"
use_ssl = true
validate_ssl = true
}
monitored_resource {
type = "uptime_url"
labels = {
project_id = var.project
host = var.static_site
}
}
}
resource "google_monitoring_alert_policy" "static_site_alert_policy" {
display_name = "Static Site Alert Policy"
combiner = "OR"
conditions {
display_name = "SSL certificate expiring soon"
condition_threshold {
filter = "metric.type=\"monitoring.googleapis.com/uptime_check/time_until_ssl_cert_expires\" AND resource.type=\"uptime_url\""
duration = "600s"
comparison = "COMPARISON_LT"
threshold_value = 15
trigger {
count = 1
}
aggregations {
alignment_period = "1200s"
per_series_aligner = "ALIGN_NEXT_OLDER"
cross_series_reducer = "REDUCE_MEAN"
group_by_fields = ["resource.label.*"]
}
}
}
user_labels = {
"uptime" = "ssl_cert_expiration"
"version" = "1"
}
notification_channels = [ google_monitoring_notification_channel.basic.name ]
}
view raw monitoring.tf hosted with ❤ by GitHub
# Add the bucket as a CDN backend
resource "google_compute_backend_bucket" "static_site" {
provider = google
name = "${var.project}-backend"
description = "Contains files needed by the website"
bucket_name = google_storage_bucket.static_site.name
enable_cdn = true
depends_on = [google_dns_record_set.static_site_txt]
}
# HTTPS certificate
resource "google_compute_managed_ssl_certificate" "static_site" {
provider = google-beta
name = "${var.project}-cert"
managed {
domains = [google_dns_record_set.static_site.name]
}
}
# GCP URL MAP (frontend config)
resource "google_compute_url_map" "static_site" {
provider = google
name = "${var.project}-url-map"
default_service = google_compute_backend_bucket.static_site.self_link
}
# GCP proxy (load balancer)
resource "google_compute_target_https_proxy" "static_site" {
provider = google
name = "${var.project}-target-proxy"
url_map = google_compute_url_map.static_site.self_link
ssl_certificates = [google_compute_managed_ssl_certificate.static_site.self_link]
}
# GCP forwarding rule (IP <-> Load Balancer mapping)
resource "google_compute_global_forwarding_rule" "default" {
provider = google
name = "${var.project}-forwarding-rule"
load_balancing_scheme = "EXTERNAL"
ip_address = google_compute_global_address.static_sites.address
ip_protocol = "TCP"
port_range = "443"
target = google_compute_target_https_proxy.static_site.self_link
}
view raw routing.tf hosted with ❤ by GitHub
resource "google_storage_bucket" "static_site" {
name = var.static_site
force_destroy = true
uniform_bucket_level_access = false
website {
main_page_suffix = "index.html"
not_found_page = "404.html"
}
cors {
origin = ["https://${var.static_site}"]
method = ["GET", "HEAD", "PUT", "POST", "DELETE"]
response_header = ["*"]
max_age_seconds = 3600
}
}
resource "google_storage_default_object_acl" "static_site_read" {
bucket = google_storage_bucket.static_site.name
role_entity = ["READER:allUsers"]
}
view raw storage.tf hosted with ❤ by GitHub
variable "project" {
default = "pcostesi-dev"
}
variable "credentials" {
default = "../credentials.json"
}
variable "region" {
default = "us-central1"
}
variable "zone" {
default = "us-central1-c"
}
variable "name" {
default = "main"
}
variable "static_site" {
default = "pcostesi.dev"
}
variable "email_address" {}
variable "site_verification" {}
view raw variables.tf hosted with ❤ by GitHub

Closing Thoughts

Cloud services have impressive offerings, and Terraform exposes them through a common language. This allows us to "measure twice before cutting" (by means of terraform plan) and to do it in an industrial scale (terraform apply and modules).

We have automated both the blog deployment process as well as the deployment process of the deployment process. So meta! 😜

In reality, we have done more than that: the declarative nature of the process also allows us to quickly correct any state drift, and with it lessen the time invested in maintenance.