# Terraform recipe: domain-level redirects using Cloudflare

If you work for an organization of a sufficient size with strong brand recognition, you may encounter the problem of needing to buy any domain names remotely similar to your canonical domain name.

A minor benefit is that visitors can make mistakes typing your domain and still get where they need to go. A **major** benefit is mitigating phishing attacks.

A classic example that I have recently seen is DHL phishing attacks. They'll send a text message claiming that you need to pay tariffs on an international shipment before it clears customs. You land on a domain name like **dlh.com** instead of **dhl.com** and enter your credit card number to realize their site is broken. Then the real bad news settles in.

Thankfully a totally more legitimate company in another industry owns dlh.com. But the lesson remains important nevertheless.

Lesson aside, how do you manage this at scale when you're using Terraform and Cloudflare? Create a module that implements a Cloudflare Redirect Rule for a Cloudflare Zone. Add a `main.tf`, `variables.tf`, and `outputs.tf` file for said module.

```plaintext
# ./modules/domain_redirect/variables.tf
variable cloudflare_account_id {
  type = string
}

variable origin {
  type = string
  description = "Source hostname"
}

variable destination {
  type = string
  description = "Target hostname"
}
```

```plaintext
# ./modules/domain_redirect/main.tf
terraform {
  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.21.0"
    }
  }
}

resource cloudflare_zone zone {
  account_id = var.cloudflare_account_id
  zone = "${var.origin}"
}

resource cloudflare_record stub {
  zone_id = cloudflare_zone.zone.id
  comment = "Stubbed record needed to trigger DNS resolution and subsequent redirect"
  type = "A"
  name = "@"
  value = "192.0.2.1"
  proxied = true
  ttl = 1
}

resource cloudflare_ruleset redirect {
  zone_id = cloudflare_zone.zone.id
  name = "Hostname redirect to ${var.destination}"
  kind = "zone"
  phase = "http_request_dynamic_redirect"
  rules {
    action = "redirect"
    action_parameters {
      from_value {
        status_code = 301
        target_url {
          value = "https://${var.destination}"
        }
        preserve_query_string = true
      }
    }
    expression  = "http.host eq \"${var.origin}\""
    description = "Redirect all requests from ${var.origin} to ${var.destination}"
    enabled     = true
  }
}
```

You'll notice the DNS record above is called a stub. This is because Cloudflare does not execute any rulesets for a given zone until there is at least one root A or CNAME record. In my example above, I use a [reserved IPv4 address](https://en.wikipedia.org/wiki/Reserved_IP_addresses#IPv4) belonging to the TEST-NET-1 IP block designated for documentation purposes by the IANA.

And in your parent `main.tf` file, you can use a `for_each` loop to map over every redirect you need to implement like so:

```plaintext
# ./main.tf
variable domain_redirects {
  type = map(string)
  default = {
    "example1.com" = "example.com"
    "example2.com" = "example.com"
    "example1.fr" = "example.fr"
  }
}

module domain_redirects {
  cloudflare_account_id = local.cloudflare_account_id
  source = "./modules/domain_redirect"
  for_each = var.domain_redirects
  origin = each.key
  destination = each.value
}
```

The benefit of this `map(string)` type variable is also that you can have multiple destinations in case your company has canonical domains for different use cases (like `example.ai`) or regions (like `example.fr`).

Lastly, you'll want to create an output value so that you'll see a more human-readable result for your domain-level redirects. First, you'll need to create an outputs file for your module:

```plaintext
#./module/domain_redirect/outputs.tf
output domain_redirect {
  value = cloudflare_ruleset.redirect.rules[0].action_parameters[0].from_value[0].target_url[0].value
}
```

And then add another `outputs.tf` file to the parent folder where your `domain_redirect` module is used:

```plaintext
# ./outputs.tf
output domain_redirects {
  value = { for domain, redirect in module.domain_redirects : domain => redirect.domain_redirect }
  description = "The target URLs for each domain redirect"
}
```

After that, you get human readable output when you run your `terraform plan` command.

```plaintext
module.domain_redirects["example1.com"].cloudflare_zone.zone: Refreshing state... [id=...]
module.domain_redirects["example1.com"].cloudflare_record.stub: Refreshing state... [id=...]
module.domain_redirects["example1.com"].cloudflare_ruleset.redirect: Refreshing state... [id=...]
module.domain_redirects["example2.com"].cloudflare_zone.zone: Refreshing state... [id=...]
module.domain_redirects["example2.com"].cloudflare_record.stub: Refreshing state... [id=...]
module.domain_redirects["example2.com"].cloudflare_ruleset.redirect: Refreshing state... [id=...]
module.domain_redirects["example1.fr"].cloudflare_zone.zone: Refreshing state... [id=...]
module.domain_redirects["example1.fr"].cloudflare_record.stub: Refreshing state... [id=...]
module.domain_redirects["example1.fr"].cloudflare_ruleset.redirect: Refreshing state... [id=...]
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

domain_redirects = {
  "example1.com" = "https://example.com"
  "example2.com" = "https://example.com"
  "example1.fr" = "https://example.fr"
}
```

That's it!
