4

I am using Google Cloud Run with Pulumi (similar to Terraform). My setup for Cloud Run's domain mapping is:

  new gcp.cloudrun.DomainMapping(
    `${prefix}-domain-mapping`,
    {
      location,
      name: 'xxx',
      metadata: {
        namespace: projectId,
      },
      spec: {
        routeName: appService.name,
      },
    },
    {
      dependsOn: [appService],
    },
  )

Where appService points to an instance of Cloud Run service. This successfully creates a domain mapping to the Cloud Run service.

Next I am setting up a DNS zone with records:

  const zone = new gcp.dns.ManagedZone(`${prefix}-zone`, {
    name: `${prefix}-zone`,
    dnsName: 'xxx.',
    visibility: 'public',
  })

  const ips = ['xxx', 'xxx', 'xxx', 'xxx']
  new gcp.dns.RecordSet(
    `${prefix}-a-records`,
    {
      name: 'xxx.',
      managedZone: zone.name,
      type: 'A',
      ttl: 3600,
      rrdatas: ips,
    },
    {
      dependsOn: [zone],
      deleteBeforeReplace: true,
    },
  )

The code above works. I have a DNS zone with four A records pointing to 4 different IP-addresses, which point to a Cloud Run service. My problem is this: how do I automate the IPs, which I have hard-coded above? I want the IP-addresses of Cloud Run to be dynamically set for the A records. The ips variable has to point to the Cloud Run instance's IPs, but I can't find a way to do that.

Or perhaps I'm doing this all wrong and there's another way this should be done? My goal is that if the Cloud Run service is updated and receives new IPs, the DNS records should be automatically updated as well. I don't want to manually update addresses.

Since Pulumi is more or less equivalent to Terraform, answers in either Terraform or Pulumi are greatly appreciated!

Kai Sellgren
  • 27,954
  • 10
  • 75
  • 87
  • Are you mapping a DNS Zone (root domain) or a subdomain of a DNS Zone? For the first case, Cloud Run provides an IP address for an A record. For the second case, Cloud Run provides a DNS name for a CNAME record. – John Hanley Mar 01 '20 at 20:31
  • I'm mapping a root domain. I can get the IPs from the Cloud Run domain mapping table, but I want that to be automated. – Kai Sellgren Mar 01 '20 at 20:42
  • If you assign `DomainMapping` to a `mapping` variable, won't `mapping.resourceRecords` contain what you need? – Mikhail Shilkov Mar 01 '20 at 20:50
  • It has a list of records, which have the rrdatas field. However, those records won't be available at the point of running the Pulumi automation. And the API won't accept resourceRecords for the rrdatas field in RecordSet. – Kai Sellgren Mar 01 '20 at 21:04
  • 1
    They will be available during the Pulumi run -- after the `DomainMapping` resource is created but before the `RecordSet` is created -- and you can assign it, that's why `rrdatas` has `Input` type, not just string array. – Mikhail Shilkov Mar 01 '20 at 21:07
  • @MikhailShilkov `rrdatas: mapping.status.resourceRecords` does not type check. `rrdatas` expects `pulumi.Input[]>`, but `resourceRecords` is `outputs.cloudrun.DomainMappingStatusResourceRecord[]`. – Kai Sellgren Mar 02 '20 at 19:41

3 Answers3

7

Since this question is tagged with both Pulumi and Terraform tags, here's a possible Terraform solution:

resource "google_cloud_run_domain_mapping" "example" {
  location = "us-central1"
  name     = "xxx"

  metadata {
    namespace = local.project_name
  }

  spec {
    route_name = google_cloud_run_service.app.name
  }
}

resource "google_dns_managed_zone" "example" {
  name       = "${local.prefix}-zone"
  dns_name   = "xxx."
  visibility = "public"
}

locals {
  dns_records = {
    "A" = [
      for rr in google_cloud_run_domain_mapping.example.resource_records :
      rr.rrdata if rr.type == "A"
    ]
    "AAAA" = [
      for rr in google_cloud_run_domain_mapping.example.resource_records :
      rr.rrdata if rr.type == "AAAA"
    ]
  }
}

resource "google_dns_record_set" "example" {
  for_each = local.dns_records

  managed_zone = google_dns_managed_zone.example.name

  name    = "xxx."
  type    = each.key
  ttl     = 3600
  rrdatas = each.value
}
Martin Atkins
  • 62,420
  • 8
  • 120
  • 138
  • 3
    I think the resource you need to iterate over is `google_cloud_run_domain_mapping.example.status[0].resource_records`. – ontek May 14 '20 at 02:22
3

I haven't tried running this but at least this code compiles:

const mapping = new gcp.cloudrun.DomainMapping(...);

const records = mapping.status.resourceRecords?.apply(rs => rs ?? []) ?? pulumi.output([]);

new gcp.dns.RecordSet(`${prefix}-a-records`, {
    name: 'xxx.',
    managedZone: zone.name,
    type: 'A',
    ttl: 3600,
    rrdatas: records.apply(rs => rs.map(r => r.rrdata)),
}, {
    dependsOn: [zone, mapping],
    deleteBeforeReplace: true,
});

The dance with records is there to get rid of undefined on two levels... Not sure if it can be simplified.

Mikhail Shilkov
  • 34,128
  • 3
  • 68
  • 107
  • This worked well. I wasn't aware of `apply` function. It kind of lets me do an "inner map". I also used it to inner filter the array to only have A-records for the A RecordSet. Thanks! – Kai Sellgren Mar 02 '20 at 21:39
  • 1
    Anytime you have a question about `Output` or `Input`, `apply` is usually the answer. Be sure to study https://www.pulumi.com/docs/intro/concepts/programming-model/#outputs – Mikhail Shilkov Mar 02 '20 at 21:58
0

We had same issue but more complicated, with for each involved. This is how we solved the issue

locals {
  regional_configs_map = {for regional_config in var.regional_configs:  regional_config.region => regional_config}
}

resource "google_cloud_run_domain_mapping" "cloudrun_mapping" {
  for_each = var.create_external_lb == false ? local.regional_configs_map : {}
  location = "${each.value.region}"
  name     = "acrossable.com"

  metadata {
    namespace = var.project
  }

  spec {
    route_name = google_cloud_run_service.cloud_run_service[each.key].name
  }
}


locals {
  dns_records = { for key, item in local.regional_configs_map :
    "${key}" => {
      "A" = [
        for rr in google_cloud_run_domain_mapping.cloudrun_mapping[key].status[0].resource_records :
        rr.rrdata if rr.type == "A"
      ]
      "AAAA" = [
        for rr in google_cloud_run_domain_mapping.cloudrun_mapping[key].status[0].resource_records :
        rr.rrdata if rr.type == "AAAA"
      ]
    }
  }
}

resource "google_dns_record_set" "dns_cloudrun_a" {
  for_each = var.create_external_lb == false ? local.regional_configs_map : {}

  project = var.dns_project
  name    = "${each.value.domain}."
  type    = "A"
  ttl     = 300

  managed_zone = var.dns_zone

  rrdatas = local.dns_records[each.key]["A"]
}

resource "google_dns_record_set" "dns_cloudrun_4a" {
  for_each = var.create_external_lb == false ? local.regional_configs_map : {}

  project = var.dns_project
  name    = "${each.value.domain}."
  type    = "AAAA"
  ttl     = 300

  managed_zone = var.dns_zone

  rrdatas = local.dns_records[each.key]["AAAA"]
}