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" |
|
} |
|
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"] |
|
# 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}"] |
|
} |
|
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 |
|
} |
|
|
|
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 ] |
|
} |
|
|
|
# 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 |
|
} |
|
|
|
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"] |
|
} |
|
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" {} |
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" |
|
} |
|
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"] |
|
# 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}"] |
|
} |
|
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 |
|
} |
|
|
|
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 ] |
|
} |
|
|
|
# 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 |
|
} |
|
|
|
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"] |
|
} |
|
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" {} |
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" |
|
} |
|
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"] |
|
# 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}"] |
|
} |
|
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 |
|
} |
|
|
|
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 ] |
|
} |
|
|
|
# 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 |
|
} |
|
|
|
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"] |
|
} |
|
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" {} |
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" |
|
} |
|
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"] |
|
# 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}"] |
|
} |
|
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 |
|
} |
|
|
|
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 ] |
|
} |
|
|
|
# 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 |
|
} |
|
|
|
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"] |
|
} |
|
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" {} |
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" |
|
} |
|
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"] |
|
# 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}"] |
|
} |
|
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 |
|
} |
|
|
|
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 ] |
|
} |
|
|
|
# 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 |
|
} |
|
|
|
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"] |
|
} |
|
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" {} |
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" |
|
} |
|
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"] |
|
# 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}"] |
|
} |
|
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 |
|
} |
|
|
|
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 ] |
|
} |
|
|
|
# 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 |
|
} |
|
|
|
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"] |
|
} |
|
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" {} |
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" |
|
} |
|
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"] |
|
# 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}"] |
|
} |
|
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 |
|
} |
|
|
|
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 ] |
|
} |
|
|
|
# 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 |
|
} |
|
|
|
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"] |
|
} |
|
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" {} |
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" |
|
} |
|
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"] |
|
# 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}"] |
|
} |
|
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 |
|
} |
|
|
|
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 ] |
|
} |
|
|
|
# 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 |
|
} |
|
|
|
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"] |
|
} |
|
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" {} |
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.