Skip to content

Terraform

Terraform is used to manage the network and hardware settings.

  • Directoryterraform/ network/hardware
    • backend.tf configures the state file
    • bunny.tf configures the CDN for media files
    • dns.tf configures domain stuff
    • providers.tf defines the providers I’m using
    • vars.tf sets what to pass in when running Terraform
    • web.tf configures my web server
terraform/dns.tf
locals {
default_ttl = 1 # auto
default_proxied = false
}
data "cloudflare_zones" "zones" {
name = local.domain
}
resource "cloudflare_dns_record" "record" {
for_each = local.records
zone_id = data.cloudflare_zones.zones.result[0].id
name = each.value.name
type = each.value.type
content = each.value.content
ttl = try(each.value.ttl, local.default_ttl)
proxied = try(each.value.proxied, local.default_proxied)
priority = try(each.value.priority, null)
}

Here Terraform uses Cloudflare to set all the records for my domain.

terraform/web.tf
resource "mythicbeasts_pi" "web" {
identifier = var.pi_identifier
disk_size = 50
model = 4
memory = 4096
}
resource "mythicbeasts_proxy_endpoint" "endpoint" {
for_each = local.web_subdomains
domain = local.domain
hostname = each.value
address = mythicbeasts_pi.web.ip
site = "all"
proxy_protocol = true
}
resource "ansible_host" "web" {
name = "pi"
groups = ["web"]
variables = {
ansible_host = "ssh.${mythicbeasts_pi.web.identifier}.hostedpi.com"
ansible_port = mythicbeasts_pi.web.ssh_port
ansible_user = "ops"
}
}

Here Terraform requests the Pi and configures the Proxy endpoints using its IP address.

It also creates an ansible_host for the Pi so that Ansible can target it to configure it.

terraform/bunny.tf
locals {
media_hostname = "media.${local.domain}"
media_storage_region = "DE"
media_s3_region = lower(local.media_storage_region)
}
resource "bunnynet_storage_zone" "media" {
name = "paultibbetts-uk-media-storage"
region = local.media_storage_region
zone_tier = "Standard"
type = "S3"
}
resource "bunnynet_pullzone" "media" {
name = "paultibbetts-uk-media-cdn"
origin {
type = "StorageZone"
storagezone = bunnynet_storage_zone.media.id
}
routing {
tier = "Standard"
zones = ["EU", "US"]
}
cache_enabled = true
cache_chunked = true
block_no_referer = false
block_post_requests = true
strip_cookies = true
}
resource "bunnynet_pullzone_hostname" "media" {
pullzone = bunnynet_pullzone.media.id
name = local.media_hostname
tls_enabled = true
force_ssl = true
depends_on = [cloudflare_dns_record.record["media"]]
}
output "media_store" {
value = {
bucket = bunnynet_storage_zone.media.name
access_key = bunnynet_storage_zone.media.name
endpoint = "https://${local.media_s3_region}-s3.storage.bunnycdn.com"
public_base_url = "https://${local.media_hostname}"
region = local.media_s3_region
}
}
output "media_store_secret_key" {
value = bunnynet_storage_zone.media.password
sensitive = true
}

Terraform uses this file to configure the “Storage Zone” (bucket) to store media files in and the “Pull Zone” (CDN) to serve them.

The state file, that records the current state of the infrastructure that Terraform manages, is hosted on MinIO running on my NAS in my homelab.

This is configured using:

terraform/backend.tf
terraform {
backend "s3" {
bucket = "tfstate"
key = "paultibbetts.uk.tfstate"
region = "main"
skip_credentials_validation = true
skip_requesting_account_id = true
skip_metadata_api_check = true
skip_region_validation = true
use_path_style = true
}
}

The variables that are my setup go in terraform/locals.tf:

terraform/locals.tf
locals {
domain = "paultibbetts.uk"
web_subdomains = {
apex = "@"
www = "www"
infra = "infra"
dev = "dev"
}
proxy_records = {
for key, host in local.web_subdomains : key => {
type = "CNAME"
name = host
content = "proxy.mythic-beasts.com"
}
}
records = merge(
local.proxy_records,
{
micro = {
type = "CNAME"
name = "micro"
content = "eu.micro.blog"
}
media = {
type = "CNAME"
name = "media"
content = bunnynet_pullzone.media.cdn_domain
}
microsoft_webmaster_tools = {
type = "CNAME"
name = "7a1aef5690ea3c9c1ba23cb97b83da6d"
content = "verify.bing.com"
}
google_search_console = {
type = "TXT"
name = "@"
content = "google-site-verification=KdhaJN7FNL4MnESeK7HbdYbnv2M6LY602PnIMWQ6BQw"
}
atproto = {
type = "TXT"
name = "_atproto"
content = "did=did:plc:adwdgyaga5q2psrjbvo4pndr"
}
github_pages_challenge = {
type = "TXT"
name = "_github-pages-challenge-paultibbetts"
content = "0d468c5988c95ab09a88f0c494dcb9"
}
mythic_beasts_challenge = {
type = "TXT"
name = "_mythic-beasts-challenge"
content = "T7eS19QKZV6VOo0og5sn2OmKDoem4dLL7IgT9tJLT7c"
}
openai_domain_verification = {
type = "TXT"
name = "@"
content = "openai-domain-verification=dv-CPJLyYLfDZ990DDlefMkxdkN"
}
# Apple iCloud email
# https://support.apple.com/en-gb/102374
apple_domain_verificaton = {
type = "TXT"
name = "@"
content = "apple-domain=JTK5xOaQo9cwujIv"
}
spf = {
type = "TXT"
name = "@"
content = "v=spf1 include:icloud.com ~all"
}
mx_icloud_01 = {
type = "MX"
name = "@"
content = "mx01.mail.icloud.com"
priority = 10
}
mx_icloud_02 = {
type = "MX"
name = "@"
content = "mx02.mail.icloud.com"
priority = 10
}
dkim_sig1 = {
type = "CNAME"
name = "sig1._domainkey"
content = "sig1.dkim.paultibbetts.uk.at.icloudmailadmin.com"
}
dmarc = {
type = "TXT"
name = "_dmarc"
content = "v=DMARC1; p=none"
}
}
)
}

Terraform needs a few secrets passing to it to work.

Because it manages the domain using Cloudflare it needs a Cloudflare API token passing to it.

MinIO, which is where the state file is hosted, also needs to be told the key to use.

Media files are served through the Bunny.net CDN, and that needs an API token to configure.

These secrets are exported as environment variables and read by Terraform:

Terminal window
source secrets.sh

where secrets.sh:

terraform/secrets.sh
export TF_VAR_cloudflare_api_token=$(pass cloudflare_api_token_paultibbettsuk)
export TF_VAR_bunnynet_api_key=$(pass bunnynet_api_key_paultibbettsuk)
export AWS_ENDPOINT_URL_S3=$(pass minio_endpoint)
export AWS_ACCESS_KEY_ID=$(pass minio_access_key)
export AWS_SECRET_ACCESS_KEY=$(pass minio_secret_key)
export MYTHICBEASTS_KEYID=$(pass mythicbeasts_keyid)
export MYTHICBEASTS_SECRET=$(pass mythicbeasts_secret)

The script grabs the secrets from a pass vault before exporting them for future commands to use.