In the last blog post, we set up Terraform to automatically provision our infrastructure so we could host this blog. Now, I'm going to focus on a pain point I found after the first month of use.
A word of caution
Always set up billing alerts and budgets, or you will find out that something as easy as hosting a blog can end up being expensive if you are not careful.
If you are using Terraform, you might be interested in checking out Infracost.
The Problem
GCP is rather pricy when it comes to serving https traffic. That's because the Load Balancer we need for TLS termination has a fixed price per hour for each forwarding rule.
However, HTTP hosting is essentially free, so now we have to find out a way to replace the Load Balancer with something else.
The Solution
Alex Olivier came up with a solution I think is great for this use-case. We don't need end-to-end encryption for a blog, but we do need encryption for .dev
domains.
We are going to use Terraform to do it, though.
Enter CloudFlare
We first have to set-up a CloudFlare account and to get the "Global API Key" under the Api Tokens page.
We then have to add that key to our secrets.auto.tfvars
file, together with the email we used to set-up the account. My secrets.auto.tfvars
looks something like this:
The main.tf
file now has a new cloudflare
provider:
|
resource "google_cloudbuild_trigger" "site_trigger" { |
|
provider = google-beta |
|
name = "site-trigger" |
|
project = var.project |
|
|
|
github { |
|
push { |
|
branch = "master" |
|
} |
|
owner = "pcostesi" |
|
name = "pcostesi.dev" |
|
|
|
} |
|
|
|
filename = "cloudbuild.yaml" |
|
} |
|
# 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" |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# Managed DNS zone |
|
data "google_dns_managed_zone" "main_site" { |
|
provider = google |
|
name = var.project |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# 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[0].name |
|
rrdatas = [google_compute_global_address.static_sites[0].address] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# 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[0].name |
|
rrdatas = ["google-site-verification=KNrjsg5IVfr7ivKz94SoZ06U7nTtfOsmuzZRncbXpRI"] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
resource "cloudflare_zone" "main_site" { |
|
zone = var.static_site |
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_zone_settings_override" "main_site" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
settings { |
|
ssl = "full" |
|
always_use_https = "on" |
|
automatic_https_rewrites = "on" |
|
} |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_record" "static_site" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
name = "${var.static_site}." |
|
value = "c.storage.googleapis.com" |
|
type = "CNAME" |
|
ttl = 1 |
|
proxied = true |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_record" "static_site_txt" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
name = "${var.static_site}." |
|
type = "TXT" |
|
ttl = 300 |
|
value = "google-site-verification=${var.google_site_verification}" |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
terraform { |
|
required_providers { |
|
google = { |
|
source = "hashicorp/google" |
|
} |
|
google-beta = { |
|
source = "hashicorp/google-beta" |
|
} |
|
cloudflare = { |
|
source = "cloudflare/cloudflare" |
|
version = "~> 2.0" |
|
} |
|
} |
|
backend "gcs"{ |
|
bucket = "terraform-static-state" |
|
prefix = "blog" |
|
credentials = "../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 |
|
} |
|
|
|
provider "cloudflare" { |
|
email = var.email_address |
|
api_key = var.cf_api |
|
} |
|
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 = "static-site-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 = "static-site-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] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# HTTPS certificate |
|
resource "google_compute_managed_ssl_certificate" "static_site" { |
|
provider = google-beta |
|
name = "static-site-cert" |
|
managed { |
|
domains = [google_dns_record_set.static_site[0].name] |
|
} |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP URL MAP (frontend config) |
|
resource "google_compute_url_map" "static_site" { |
|
provider = google |
|
name = "static-site-url-map" |
|
default_service = google_compute_backend_bucket.static_site[0].self_link |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP proxy (load balancer) |
|
resource "google_compute_target_https_proxy" "static_site" { |
|
provider = google |
|
name = "static-site-target-proxy" |
|
url_map = google_compute_url_map.static_site[0].self_link |
|
ssl_certificates = [google_compute_managed_ssl_certificate.static_site[0].self_link] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP forwarding rule (IP <-> Load Balancer mapping) |
|
resource "google_compute_global_forwarding_rule" "default" { |
|
provider = google |
|
name = "static-site-forwarding-rule" |
|
load_balancing_scheme = "EXTERNAL" |
|
ip_address = google_compute_global_address.static_sites[0].address |
|
ip_protocol = "TCP" |
|
port_range = "443" |
|
target = google_compute_target_https_proxy.static_site[0].self_link |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
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 "google_site_verification" { |
|
default = "KNrjsg5IVfr7ivKz94SoZ06U7nTtfOsmuzZRncbXpRI" |
|
} |
|
|
|
variable "use_cloudflare" { |
|
default = true |
|
} |
|
|
|
variable "email_address" {} |
|
|
|
variable "cf_api" {} |
This allows us to use the CloudFlare API from Terraform.
Networking
Since we're moving our public layer to another provider, some things are going to change in the networking section. Most notably, we now have to point our domain to CloudFlare's own load balancers instead of Google's, and to fetch the resources from the bucket for us.
This means we need to remove the old configuration and replace it with an equivalent version using CloudFlare. However, we might want to switch back to the older config! Although we've got this under version control, going back and forth between commits to conditionally deploy resources is not a good practice. In order to avoid this, we're going to use a little hack: leverage the count
field in the terraform settings to decide if we want this resource to be deployed or not.
|
resource "google_cloudbuild_trigger" "site_trigger" { |
|
provider = google-beta |
|
name = "site-trigger" |
|
project = var.project |
|
|
|
github { |
|
push { |
|
branch = "master" |
|
} |
|
owner = "pcostesi" |
|
name = "pcostesi.dev" |
|
|
|
} |
|
|
|
filename = "cloudbuild.yaml" |
|
} |
|
# 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" |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# Managed DNS zone |
|
data "google_dns_managed_zone" "main_site" { |
|
provider = google |
|
name = var.project |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# 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[0].name |
|
rrdatas = [google_compute_global_address.static_sites[0].address] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# 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[0].name |
|
rrdatas = ["google-site-verification=KNrjsg5IVfr7ivKz94SoZ06U7nTtfOsmuzZRncbXpRI"] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
resource "cloudflare_zone" "main_site" { |
|
zone = var.static_site |
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_zone_settings_override" "main_site" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
settings { |
|
ssl = "full" |
|
always_use_https = "on" |
|
automatic_https_rewrites = "on" |
|
} |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_record" "static_site" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
name = "${var.static_site}." |
|
value = "c.storage.googleapis.com" |
|
type = "CNAME" |
|
ttl = 1 |
|
proxied = true |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_record" "static_site_txt" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
name = "${var.static_site}." |
|
type = "TXT" |
|
ttl = 300 |
|
value = "google-site-verification=${var.google_site_verification}" |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
terraform { |
|
required_providers { |
|
google = { |
|
source = "hashicorp/google" |
|
} |
|
google-beta = { |
|
source = "hashicorp/google-beta" |
|
} |
|
cloudflare = { |
|
source = "cloudflare/cloudflare" |
|
version = "~> 2.0" |
|
} |
|
} |
|
backend "gcs"{ |
|
bucket = "terraform-static-state" |
|
prefix = "blog" |
|
credentials = "../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 |
|
} |
|
|
|
provider "cloudflare" { |
|
email = var.email_address |
|
api_key = var.cf_api |
|
} |
|
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 = "static-site-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 = "static-site-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] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# HTTPS certificate |
|
resource "google_compute_managed_ssl_certificate" "static_site" { |
|
provider = google-beta |
|
name = "static-site-cert" |
|
managed { |
|
domains = [google_dns_record_set.static_site[0].name] |
|
} |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP URL MAP (frontend config) |
|
resource "google_compute_url_map" "static_site" { |
|
provider = google |
|
name = "static-site-url-map" |
|
default_service = google_compute_backend_bucket.static_site[0].self_link |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP proxy (load balancer) |
|
resource "google_compute_target_https_proxy" "static_site" { |
|
provider = google |
|
name = "static-site-target-proxy" |
|
url_map = google_compute_url_map.static_site[0].self_link |
|
ssl_certificates = [google_compute_managed_ssl_certificate.static_site[0].self_link] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP forwarding rule (IP <-> Load Balancer mapping) |
|
resource "google_compute_global_forwarding_rule" "default" { |
|
provider = google |
|
name = "static-site-forwarding-rule" |
|
load_balancing_scheme = "EXTERNAL" |
|
ip_address = google_compute_global_address.static_sites[0].address |
|
ip_protocol = "TCP" |
|
port_range = "443" |
|
target = google_compute_target_https_proxy.static_site[0].self_link |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
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 "google_site_verification" { |
|
default = "KNrjsg5IVfr7ivKz94SoZ06U7nTtfOsmuzZRncbXpRI" |
|
} |
|
|
|
variable "use_cloudflare" { |
|
default = true |
|
} |
|
|
|
variable "email_address" {} |
|
|
|
variable "cf_api" {} |
Terraform allows for conditonal expressions and arbitrary values through the use of ternary if operators. This enables us to check a variable (such as use_cloudflare
) to deploy just the resources we want.
That variable is located in the updated variables.tf
file and can be overriden by the CLI:
|
resource "google_cloudbuild_trigger" "site_trigger" { |
|
provider = google-beta |
|
name = "site-trigger" |
|
project = var.project |
|
|
|
github { |
|
push { |
|
branch = "master" |
|
} |
|
owner = "pcostesi" |
|
name = "pcostesi.dev" |
|
|
|
} |
|
|
|
filename = "cloudbuild.yaml" |
|
} |
|
# 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" |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# Managed DNS zone |
|
data "google_dns_managed_zone" "main_site" { |
|
provider = google |
|
name = var.project |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# 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[0].name |
|
rrdatas = [google_compute_global_address.static_sites[0].address] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# 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[0].name |
|
rrdatas = ["google-site-verification=KNrjsg5IVfr7ivKz94SoZ06U7nTtfOsmuzZRncbXpRI"] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
resource "cloudflare_zone" "main_site" { |
|
zone = var.static_site |
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_zone_settings_override" "main_site" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
settings { |
|
ssl = "full" |
|
always_use_https = "on" |
|
automatic_https_rewrites = "on" |
|
} |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_record" "static_site" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
name = "${var.static_site}." |
|
value = "c.storage.googleapis.com" |
|
type = "CNAME" |
|
ttl = 1 |
|
proxied = true |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_record" "static_site_txt" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
name = "${var.static_site}." |
|
type = "TXT" |
|
ttl = 300 |
|
value = "google-site-verification=${var.google_site_verification}" |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
terraform { |
|
required_providers { |
|
google = { |
|
source = "hashicorp/google" |
|
} |
|
google-beta = { |
|
source = "hashicorp/google-beta" |
|
} |
|
cloudflare = { |
|
source = "cloudflare/cloudflare" |
|
version = "~> 2.0" |
|
} |
|
} |
|
backend "gcs"{ |
|
bucket = "terraform-static-state" |
|
prefix = "blog" |
|
credentials = "../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 |
|
} |
|
|
|
provider "cloudflare" { |
|
email = var.email_address |
|
api_key = var.cf_api |
|
} |
|
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 = "static-site-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 = "static-site-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] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# HTTPS certificate |
|
resource "google_compute_managed_ssl_certificate" "static_site" { |
|
provider = google-beta |
|
name = "static-site-cert" |
|
managed { |
|
domains = [google_dns_record_set.static_site[0].name] |
|
} |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP URL MAP (frontend config) |
|
resource "google_compute_url_map" "static_site" { |
|
provider = google |
|
name = "static-site-url-map" |
|
default_service = google_compute_backend_bucket.static_site[0].self_link |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP proxy (load balancer) |
|
resource "google_compute_target_https_proxy" "static_site" { |
|
provider = google |
|
name = "static-site-target-proxy" |
|
url_map = google_compute_url_map.static_site[0].self_link |
|
ssl_certificates = [google_compute_managed_ssl_certificate.static_site[0].self_link] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP forwarding rule (IP <-> Load Balancer mapping) |
|
resource "google_compute_global_forwarding_rule" "default" { |
|
provider = google |
|
name = "static-site-forwarding-rule" |
|
load_balancing_scheme = "EXTERNAL" |
|
ip_address = google_compute_global_address.static_sites[0].address |
|
ip_protocol = "TCP" |
|
port_range = "443" |
|
target = google_compute_target_https_proxy.static_site[0].self_link |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
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 "google_site_verification" { |
|
default = "KNrjsg5IVfr7ivKz94SoZ06U7nTtfOsmuzZRncbXpRI" |
|
} |
|
|
|
variable "use_cloudflare" { |
|
default = true |
|
} |
|
|
|
variable "email_address" {} |
|
|
|
variable "cf_api" {} |
Routing
It's not always just DNS. We need to disable the Load Balancer and all of the expensive routing components (this is, in fact, the most important part). We simply use the count
trick to disable everything here.
|
resource "google_cloudbuild_trigger" "site_trigger" { |
|
provider = google-beta |
|
name = "site-trigger" |
|
project = var.project |
|
|
|
github { |
|
push { |
|
branch = "master" |
|
} |
|
owner = "pcostesi" |
|
name = "pcostesi.dev" |
|
|
|
} |
|
|
|
filename = "cloudbuild.yaml" |
|
} |
|
# 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" |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# Managed DNS zone |
|
data "google_dns_managed_zone" "main_site" { |
|
provider = google |
|
name = var.project |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# 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[0].name |
|
rrdatas = [google_compute_global_address.static_sites[0].address] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# 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[0].name |
|
rrdatas = ["google-site-verification=KNrjsg5IVfr7ivKz94SoZ06U7nTtfOsmuzZRncbXpRI"] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
resource "cloudflare_zone" "main_site" { |
|
zone = var.static_site |
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_zone_settings_override" "main_site" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
settings { |
|
ssl = "full" |
|
always_use_https = "on" |
|
automatic_https_rewrites = "on" |
|
} |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_record" "static_site" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
name = "${var.static_site}." |
|
value = "c.storage.googleapis.com" |
|
type = "CNAME" |
|
ttl = 1 |
|
proxied = true |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_record" "static_site_txt" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
name = "${var.static_site}." |
|
type = "TXT" |
|
ttl = 300 |
|
value = "google-site-verification=${var.google_site_verification}" |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
terraform { |
|
required_providers { |
|
google = { |
|
source = "hashicorp/google" |
|
} |
|
google-beta = { |
|
source = "hashicorp/google-beta" |
|
} |
|
cloudflare = { |
|
source = "cloudflare/cloudflare" |
|
version = "~> 2.0" |
|
} |
|
} |
|
backend "gcs"{ |
|
bucket = "terraform-static-state" |
|
prefix = "blog" |
|
credentials = "../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 |
|
} |
|
|
|
provider "cloudflare" { |
|
email = var.email_address |
|
api_key = var.cf_api |
|
} |
|
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 = "static-site-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 = "static-site-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] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# HTTPS certificate |
|
resource "google_compute_managed_ssl_certificate" "static_site" { |
|
provider = google-beta |
|
name = "static-site-cert" |
|
managed { |
|
domains = [google_dns_record_set.static_site[0].name] |
|
} |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP URL MAP (frontend config) |
|
resource "google_compute_url_map" "static_site" { |
|
provider = google |
|
name = "static-site-url-map" |
|
default_service = google_compute_backend_bucket.static_site[0].self_link |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP proxy (load balancer) |
|
resource "google_compute_target_https_proxy" "static_site" { |
|
provider = google |
|
name = "static-site-target-proxy" |
|
url_map = google_compute_url_map.static_site[0].self_link |
|
ssl_certificates = [google_compute_managed_ssl_certificate.static_site[0].self_link] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP forwarding rule (IP <-> Load Balancer mapping) |
|
resource "google_compute_global_forwarding_rule" "default" { |
|
provider = google |
|
name = "static-site-forwarding-rule" |
|
load_balancing_scheme = "EXTERNAL" |
|
ip_address = google_compute_global_address.static_sites[0].address |
|
ip_protocol = "TCP" |
|
port_range = "443" |
|
target = google_compute_target_https_proxy.static_site[0].self_link |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
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 "google_site_verification" { |
|
default = "KNrjsg5IVfr7ivKz94SoZ06U7nTtfOsmuzZRncbXpRI" |
|
} |
|
|
|
variable "use_cloudflare" { |
|
default = true |
|
} |
|
|
|
variable "email_address" {} |
|
|
|
variable "cf_api" {} |
The Rest
For the sake of completeness, I'm going to post the rest of the files.
The build process stays the same:
|
resource "google_cloudbuild_trigger" "site_trigger" { |
|
provider = google-beta |
|
name = "site-trigger" |
|
project = var.project |
|
|
|
github { |
|
push { |
|
branch = "master" |
|
} |
|
owner = "pcostesi" |
|
name = "pcostesi.dev" |
|
|
|
} |
|
|
|
filename = "cloudbuild.yaml" |
|
} |
|
# 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" |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# Managed DNS zone |
|
data "google_dns_managed_zone" "main_site" { |
|
provider = google |
|
name = var.project |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# 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[0].name |
|
rrdatas = [google_compute_global_address.static_sites[0].address] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# 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[0].name |
|
rrdatas = ["google-site-verification=KNrjsg5IVfr7ivKz94SoZ06U7nTtfOsmuzZRncbXpRI"] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
resource "cloudflare_zone" "main_site" { |
|
zone = var.static_site |
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_zone_settings_override" "main_site" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
settings { |
|
ssl = "full" |
|
always_use_https = "on" |
|
automatic_https_rewrites = "on" |
|
} |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_record" "static_site" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
name = "${var.static_site}." |
|
value = "c.storage.googleapis.com" |
|
type = "CNAME" |
|
ttl = 1 |
|
proxied = true |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_record" "static_site_txt" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
name = "${var.static_site}." |
|
type = "TXT" |
|
ttl = 300 |
|
value = "google-site-verification=${var.google_site_verification}" |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
terraform { |
|
required_providers { |
|
google = { |
|
source = "hashicorp/google" |
|
} |
|
google-beta = { |
|
source = "hashicorp/google-beta" |
|
} |
|
cloudflare = { |
|
source = "cloudflare/cloudflare" |
|
version = "~> 2.0" |
|
} |
|
} |
|
backend "gcs"{ |
|
bucket = "terraform-static-state" |
|
prefix = "blog" |
|
credentials = "../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 |
|
} |
|
|
|
provider "cloudflare" { |
|
email = var.email_address |
|
api_key = var.cf_api |
|
} |
|
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 = "static-site-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 = "static-site-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] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# HTTPS certificate |
|
resource "google_compute_managed_ssl_certificate" "static_site" { |
|
provider = google-beta |
|
name = "static-site-cert" |
|
managed { |
|
domains = [google_dns_record_set.static_site[0].name] |
|
} |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP URL MAP (frontend config) |
|
resource "google_compute_url_map" "static_site" { |
|
provider = google |
|
name = "static-site-url-map" |
|
default_service = google_compute_backend_bucket.static_site[0].self_link |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP proxy (load balancer) |
|
resource "google_compute_target_https_proxy" "static_site" { |
|
provider = google |
|
name = "static-site-target-proxy" |
|
url_map = google_compute_url_map.static_site[0].self_link |
|
ssl_certificates = [google_compute_managed_ssl_certificate.static_site[0].self_link] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP forwarding rule (IP <-> Load Balancer mapping) |
|
resource "google_compute_global_forwarding_rule" "default" { |
|
provider = google |
|
name = "static-site-forwarding-rule" |
|
load_balancing_scheme = "EXTERNAL" |
|
ip_address = google_compute_global_address.static_sites[0].address |
|
ip_protocol = "TCP" |
|
port_range = "443" |
|
target = google_compute_target_https_proxy.static_site[0].self_link |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
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 "google_site_verification" { |
|
default = "KNrjsg5IVfr7ivKz94SoZ06U7nTtfOsmuzZRncbXpRI" |
|
} |
|
|
|
variable "use_cloudflare" { |
|
default = true |
|
} |
|
|
|
variable "email_address" {} |
|
|
|
variable "cf_api" {} |
The monitor still points to the dev site (although we could add cloudflare_healthcheck
s to do the same)
|
resource "google_cloudbuild_trigger" "site_trigger" { |
|
provider = google-beta |
|
name = "site-trigger" |
|
project = var.project |
|
|
|
github { |
|
push { |
|
branch = "master" |
|
} |
|
owner = "pcostesi" |
|
name = "pcostesi.dev" |
|
|
|
} |
|
|
|
filename = "cloudbuild.yaml" |
|
} |
|
# 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" |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# Managed DNS zone |
|
data "google_dns_managed_zone" "main_site" { |
|
provider = google |
|
name = var.project |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# 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[0].name |
|
rrdatas = [google_compute_global_address.static_sites[0].address] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# 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[0].name |
|
rrdatas = ["google-site-verification=KNrjsg5IVfr7ivKz94SoZ06U7nTtfOsmuzZRncbXpRI"] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
resource "cloudflare_zone" "main_site" { |
|
zone = var.static_site |
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_zone_settings_override" "main_site" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
settings { |
|
ssl = "full" |
|
always_use_https = "on" |
|
automatic_https_rewrites = "on" |
|
} |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_record" "static_site" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
name = "${var.static_site}." |
|
value = "c.storage.googleapis.com" |
|
type = "CNAME" |
|
ttl = 1 |
|
proxied = true |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_record" "static_site_txt" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
name = "${var.static_site}." |
|
type = "TXT" |
|
ttl = 300 |
|
value = "google-site-verification=${var.google_site_verification}" |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
terraform { |
|
required_providers { |
|
google = { |
|
source = "hashicorp/google" |
|
} |
|
google-beta = { |
|
source = "hashicorp/google-beta" |
|
} |
|
cloudflare = { |
|
source = "cloudflare/cloudflare" |
|
version = "~> 2.0" |
|
} |
|
} |
|
backend "gcs"{ |
|
bucket = "terraform-static-state" |
|
prefix = "blog" |
|
credentials = "../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 |
|
} |
|
|
|
provider "cloudflare" { |
|
email = var.email_address |
|
api_key = var.cf_api |
|
} |
|
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 = "static-site-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 = "static-site-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] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# HTTPS certificate |
|
resource "google_compute_managed_ssl_certificate" "static_site" { |
|
provider = google-beta |
|
name = "static-site-cert" |
|
managed { |
|
domains = [google_dns_record_set.static_site[0].name] |
|
} |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP URL MAP (frontend config) |
|
resource "google_compute_url_map" "static_site" { |
|
provider = google |
|
name = "static-site-url-map" |
|
default_service = google_compute_backend_bucket.static_site[0].self_link |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP proxy (load balancer) |
|
resource "google_compute_target_https_proxy" "static_site" { |
|
provider = google |
|
name = "static-site-target-proxy" |
|
url_map = google_compute_url_map.static_site[0].self_link |
|
ssl_certificates = [google_compute_managed_ssl_certificate.static_site[0].self_link] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP forwarding rule (IP <-> Load Balancer mapping) |
|
resource "google_compute_global_forwarding_rule" "default" { |
|
provider = google |
|
name = "static-site-forwarding-rule" |
|
load_balancing_scheme = "EXTERNAL" |
|
ip_address = google_compute_global_address.static_sites[0].address |
|
ip_protocol = "TCP" |
|
port_range = "443" |
|
target = google_compute_target_https_proxy.static_site[0].self_link |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
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 "google_site_verification" { |
|
default = "KNrjsg5IVfr7ivKz94SoZ06U7nTtfOsmuzZRncbXpRI" |
|
} |
|
|
|
variable "use_cloudflare" { |
|
default = true |
|
} |
|
|
|
variable "email_address" {} |
|
|
|
variable "cf_api" {} |
Finally, the bucket is still there. We could also host this using AWS, but I bet you have seen a thousand blogs do the same.
|
resource "google_cloudbuild_trigger" "site_trigger" { |
|
provider = google-beta |
|
name = "site-trigger" |
|
project = var.project |
|
|
|
github { |
|
push { |
|
branch = "master" |
|
} |
|
owner = "pcostesi" |
|
name = "pcostesi.dev" |
|
|
|
} |
|
|
|
filename = "cloudbuild.yaml" |
|
} |
|
# 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" |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# Managed DNS zone |
|
data "google_dns_managed_zone" "main_site" { |
|
provider = google |
|
name = var.project |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# 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[0].name |
|
rrdatas = [google_compute_global_address.static_sites[0].address] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# 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[0].name |
|
rrdatas = ["google-site-verification=KNrjsg5IVfr7ivKz94SoZ06U7nTtfOsmuzZRncbXpRI"] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
resource "cloudflare_zone" "main_site" { |
|
zone = var.static_site |
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_zone_settings_override" "main_site" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
settings { |
|
ssl = "full" |
|
always_use_https = "on" |
|
automatic_https_rewrites = "on" |
|
} |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_record" "static_site" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
name = "${var.static_site}." |
|
value = "c.storage.googleapis.com" |
|
type = "CNAME" |
|
ttl = 1 |
|
proxied = true |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
|
|
resource "cloudflare_record" "static_site_txt" { |
|
zone_id = cloudflare_zone.main_site[0].id |
|
name = "${var.static_site}." |
|
type = "TXT" |
|
ttl = 300 |
|
value = "google-site-verification=${var.google_site_verification}" |
|
|
|
count = var.use_cloudflare ? 1 : 0 |
|
} |
|
terraform { |
|
required_providers { |
|
google = { |
|
source = "hashicorp/google" |
|
} |
|
google-beta = { |
|
source = "hashicorp/google-beta" |
|
} |
|
cloudflare = { |
|
source = "cloudflare/cloudflare" |
|
version = "~> 2.0" |
|
} |
|
} |
|
backend "gcs"{ |
|
bucket = "terraform-static-state" |
|
prefix = "blog" |
|
credentials = "../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 |
|
} |
|
|
|
provider "cloudflare" { |
|
email = var.email_address |
|
api_key = var.cf_api |
|
} |
|
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 = "static-site-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 = "static-site-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] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# HTTPS certificate |
|
resource "google_compute_managed_ssl_certificate" "static_site" { |
|
provider = google-beta |
|
name = "static-site-cert" |
|
managed { |
|
domains = [google_dns_record_set.static_site[0].name] |
|
} |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP URL MAP (frontend config) |
|
resource "google_compute_url_map" "static_site" { |
|
provider = google |
|
name = "static-site-url-map" |
|
default_service = google_compute_backend_bucket.static_site[0].self_link |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP proxy (load balancer) |
|
resource "google_compute_target_https_proxy" "static_site" { |
|
provider = google |
|
name = "static-site-target-proxy" |
|
url_map = google_compute_url_map.static_site[0].self_link |
|
ssl_certificates = [google_compute_managed_ssl_certificate.static_site[0].self_link] |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
# GCP forwarding rule (IP <-> Load Balancer mapping) |
|
resource "google_compute_global_forwarding_rule" "default" { |
|
provider = google |
|
name = "static-site-forwarding-rule" |
|
load_balancing_scheme = "EXTERNAL" |
|
ip_address = google_compute_global_address.static_sites[0].address |
|
ip_protocol = "TCP" |
|
port_range = "443" |
|
target = google_compute_target_https_proxy.static_site[0].self_link |
|
|
|
count = var.use_cloudflare ? 0 : 1 |
|
} |
|
|
|
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 "google_site_verification" { |
|
default = "KNrjsg5IVfr7ivKz94SoZ06U7nTtfOsmuzZRncbXpRI" |
|
} |
|
|
|
variable "use_cloudflare" { |
|
default = true |
|
} |
|
|
|
variable "email_address" {} |
|
|
|
variable "cf_api" {} |
Thoughts
My expenses have gone down quite significantly, as the load balancer was responsible for the largest amount by far.
But I think the most interesting trick here was to use count
and to do all this using Terraform.