Skip to content

Commit b1033e8

Browse files
bronzlemtblanton
andauthored
Use URL Masking for deployment envs (#16)
Use URL Masks for envs. This simplifies DNS and certs for environments, and allows for easy ad-hoc/ephemeral environments --------- Co-authored-by: Taylor Blanton <matthewtblanton@gmail.com>
1 parent 93def94 commit b1033e8

File tree

10 files changed

+171
-229
lines changed

10 files changed

+171
-229
lines changed

project-terraform/infra/main.tf

Lines changed: 119 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -41,69 +41,23 @@ resource "google_project_iam_member" "cloud_run_secret_access" {
4141
locals {
4242
project_name = trimprefix(var.project_id, "ai2-skiff2-")
4343

44-
# Prod (main branch) services — keys where deployment_environment is "prod"
45-
prod_services = {
46-
for key, svc in var.services : key => svc if svc.deployment_environment == "prod"
44+
domain_families = {
45+
allenai = "allen.ai"
46+
apps = "apps.allenai.org"
47+
pandajungle = "pandajungle.org"
4748
}
4849

49-
# Branch (non-prod) services
50-
branch_services = {
51-
for key, svc in var.services : key => svc if svc.deployment_environment != "prod"
52-
}
53-
54-
# Non-default prod services (for subdomain routing)
55-
non_default_prod_services = {
56-
for key, svc in local.prod_services : key => svc if key != var.default_service
57-
}
58-
59-
# Exclude allow_delete services from the URL map (prevents bug where deleting a service errors because the url map prevents the backend from being deleted)
60-
url_map_non_default_prod_services = {
61-
for key, svc in local.non_default_prod_services : key => svc if !svc.allow_delete
62-
}
63-
64-
url_map_branch_services = {
65-
for key, svc in local.branch_services : key => svc if !svc.allow_delete
66-
}
67-
68-
# Distinct branch names (non-prod)
69-
branch_names = distinct([for _, svc in local.branch_services : svc.deployment_environment])
70-
71-
# Default service key per branch (first service found for each branch)
72-
branch_default_keys = {
73-
for branch in local.branch_names : branch => [
74-
for key, svc in local.branch_services : key if svc.deployment_environment == branch
75-
][0]
76-
}
50+
primary_domain_key = "pandajungle"
7751

78-
# Non-default branch services (for branch subdomain routing)
79-
non_default_branch_services = {
80-
for key, svc in local.url_map_branch_services : key => svc
81-
if key != lookup(local.branch_default_keys, svc.deployment_environment, "")
82-
}
52+
# Base domains for this project (e.g., "myapp.allen.ai")
53+
base_domains = { for key, domain in local.domain_families : key => "${local.project_name}.${domain}" }
8354

84-
# All domains for the SSL certificate
85-
all_domains = concat(
86-
# pandajungle.org — prod
87-
["${local.project_name}.pandajungle.org"],
88-
[for key, _ in local.non_default_prod_services : "${_.name}.${local.project_name}.pandajungle.org"],
89-
90-
# apps.allenai.org and allen.ai — prod
91-
["${local.project_name}.allen.ai", "${local.project_name}.apps.allenai.org"],
92-
[for key, _ in local.non_default_prod_services : "${_.name}.${local.project_name}.allen.ai"],
93-
[for key, _ in local.non_default_prod_services : "${_.name}.${local.project_name}.apps.allenai.org"],
94-
95-
# Branch domains — branch.project.pandajungle.org for default service per branch
96-
[for branch in local.branch_names : "${branch}.${local.project_name}.pandajungle.org"],
97-
[for branch in local.branch_names : "${branch}.${local.project_name}.allen.ai"],
98-
[for branch in local.branch_names : "${branch}.${local.project_name}.apps.allenai.org"],
99-
100-
# Branch domains — branch.service.project.pandajungle.org for non-default services
101-
[for key, svc in local.non_default_branch_services : "${svc.deployment_environment}.${svc.name}.${local.project_name}.pandajungle.org"],
102-
[for key, svc in local.non_default_branch_services : "${svc.deployment_environment}.${svc.name}.${local.project_name}.allen.ai"],
103-
[for key, svc in local.non_default_branch_services : "${svc.deployment_environment}.${svc.name}.${local.project_name}.apps.allenai.org"],
104-
105-
# Per-service custom domains (prod only)
106-
flatten([for _, svc in local.prod_services : svc.custom_domains]),
55+
# All backend NEG IDs, keyed by backend name
56+
all_backends = merge(
57+
{ for key, neg in google_compute_region_network_endpoint_group.url_mask : "url-mask-${key}" => neg.id },
58+
{ "default" = google_compute_region_network_endpoint_group.default_service.id },
59+
{ for key, neg in google_compute_region_network_endpoint_group.branch_default : "branch-${key}" => neg.id },
60+
{ for key, neg in google_compute_region_network_endpoint_group.custom_domain : "custom-${replace(key, ".", "-")}" => neg.id },
10761
)
10862
}
10963

@@ -117,118 +71,164 @@ data "google_compute_security_policy" "cloud_armor" {
11771
project = var.project_id
11872
}
11973

120-
resource "google_compute_region_network_endpoint_group" "default" {
121-
for_each = var.services
122-
name = "${each.value.deployment_environment}-${each.value.name}-neg"
74+
# Create URL Mask NEGs for base service URL on each domain
75+
# <service>.project.domain to the matching Cloud Run service by name.
76+
resource "google_compute_region_network_endpoint_group" "url_mask" {
77+
for_each = local.domain_families
78+
name = "${local.project_name}-${each.key}-neg"
79+
network_endpoint_type = "SERVERLESS"
80+
region = var.region
81+
project = var.project_id
82+
cloud_run {
83+
url_mask = "<service>.${local.base_domains[each.key]}"
84+
}
85+
}
86+
87+
# Explicit NEG for prod default service (bare domain routing)
88+
resource "google_compute_region_network_endpoint_group" "default_service" {
89+
name = "${local.project_name}-default-${var.default_service}-neg"
90+
network_endpoint_type = "SERVERLESS"
91+
region = var.region
92+
project = var.project_id
93+
cloud_run {
94+
service = "prod-${var.default_service}"
95+
}
96+
lifecycle {
97+
create_before_destroy = true
98+
}
99+
}
100+
101+
# Explicit NEGs for branch default services (branch bare domain routing)
102+
resource "google_compute_region_network_endpoint_group" "branch_default" {
103+
for_each = toset(var.branch_environments)
104+
name = "${local.project_name}-${each.value}-default-${var.default_service}-neg"
105+
network_endpoint_type = "SERVERLESS"
106+
region = var.region
107+
project = var.project_id
108+
cloud_run {
109+
service = "${each.value}-${var.default_service}"
110+
}
111+
lifecycle {
112+
create_before_destroy = true
113+
}
114+
}
115+
116+
# Explicit NEGs for custom domain mappings
117+
resource "google_compute_region_network_endpoint_group" "custom_domain" {
118+
for_each = var.custom_domain_mappings
119+
name = "${local.project_name}-custom-${replace(each.key, ".", "-")}-neg"
123120
network_endpoint_type = "SERVERLESS"
124121
region = var.region
125122
project = var.project_id
126123
cloud_run {
127-
service = "${each.value.deployment_environment}-${each.value.name}"
124+
service = each.value
128125
}
129126
}
130127

131-
# Custom URL map with host-based routing for service subdomains
128+
# URL Map
129+
#
132130
resource "google_compute_url_map" "default" {
133131
name = "${local.project_name}-url-map"
134132
project = var.project_id
135133

136-
# Default service handles project_name.pandajungle.org and any unmatched hosts
137-
# NOTE: We construct self_links manually instead of referencing module.lb-http.backend_services[...].self_link
138-
# to avoid an implicit Terraform dependency that causes GCP to reject backend deletion before the URL map is updated.
139-
default_service = "projects/${var.project_id}/global/backendServices/default-lb-backend-default"
134+
# Unmatched hosts fall through to the `primary_domain_key` URL mask backend
135+
default_service = "projects/${var.project_id}/global/backendServices/default-lb-backend-url-mask-${local.primary_domain_key}"
140136

141-
# Route each non-default prod service's subdomain to its backend
137+
# Route each domain family's wildcard to its URL mask backend
142138
dynamic "host_rule" {
143-
for_each = local.url_map_non_default_prod_services
139+
for_each = local.domain_families
144140
content {
145-
hosts = concat(
146-
[
147-
"${host_rule.value.name}.${local.project_name}.pandajungle.org",
148-
"${host_rule.value.name}.${local.project_name}.allen.ai",
149-
"${host_rule.value.name}.${local.project_name}.apps.allenai.org",
150-
],
151-
host_rule.value.custom_domains,
152-
)
153-
path_matcher = host_rule.key
141+
hosts = ["*.${local.base_domains[host_rule.key]}"]
142+
path_matcher = "url-mask-${host_rule.key}"
154143
}
155144
}
156145

157146
dynamic "path_matcher" {
158-
for_each = local.url_map_non_default_prod_services
147+
for_each = local.domain_families
159148
content {
160-
name = path_matcher.key
161-
default_service = module.lb-http.backend_services[path_matcher.key].self_link
149+
name = "url-mask-${path_matcher.key}"
150+
default_service = "projects/${var.project_id}/global/backendServices/default-lb-backend-url-mask-${path_matcher.key}"
162151
}
163152
}
164153

165-
# Route branch default service: branch.project.pandajungle.org
154+
# Bare domains -> prod default service
155+
host_rule {
156+
hosts = values(local.base_domains)
157+
path_matcher = "bare-domain"
158+
}
159+
160+
path_matcher {
161+
name = "bare-domain"
162+
default_service = "projects/${var.project_id}/global/backendServices/default-lb-backend-default"
163+
}
164+
165+
# Branch bare domains -> branch default service
166166
dynamic "host_rule" {
167-
for_each = local.branch_default_keys
167+
for_each = toset(var.branch_environments)
168168
content {
169-
hosts = [
170-
"${host_rule.key}.${local.project_name}.pandajungle.org",
171-
"${host_rule.key}.${local.project_name}.allen.ai",
172-
"${host_rule.key}.${local.project_name}.apps.allenai.org",
173-
]
174-
path_matcher = "branch-${host_rule.key}"
169+
hosts = [for _, d in local.base_domains : "${host_rule.value}.${d}"]
170+
path_matcher = "branch-${host_rule.value}"
175171
}
176172
}
177173

178174
dynamic "path_matcher" {
179-
for_each = local.branch_default_keys
175+
for_each = toset(var.branch_environments)
180176
content {
181-
name = "branch-${path_matcher.key}"
182-
default_service = module.lb-http.backend_services[path_matcher.value].self_link
177+
name = "branch-${path_matcher.value}"
178+
default_service = "projects/${var.project_id}/global/backendServices/default-lb-backend-branch-${path_matcher.value}"
183179
}
184180
}
185181

186-
# Route branch non-default services: branch.service.project.pandajungle.org
182+
# Custom domains -> explicit service backends
187183
dynamic "host_rule" {
188-
for_each = local.non_default_branch_services
184+
for_each = var.custom_domain_mappings
189185
content {
190-
hosts = [
191-
"${host_rule.value.deployment_environment}.${host_rule.value.name}.${local.project_name}.pandajungle.org",
192-
"${host_rule.value.deployment_environment}.${host_rule.value.name}.${local.project_name}.allen.ai",
193-
"${host_rule.value.deployment_environment}.${host_rule.value.name}.${local.project_name}.apps.allenai.org",
194-
]
195-
path_matcher = host_rule.key
186+
hosts = [host_rule.key]
187+
path_matcher = "custom-${replace(host_rule.key, ".", "-")}"
196188
}
197189
}
198190

199191
dynamic "path_matcher" {
200-
for_each = local.non_default_branch_services
192+
for_each = var.custom_domain_mappings
201193
content {
202-
name = path_matcher.key
203-
default_service = "projects/${var.project_id}/global/backendServices/default-lb-backend-${path_matcher.key}"
194+
name = "custom-${replace(path_matcher.key, ".", "-")}"
195+
default_service = "projects/${var.project_id}/global/backendServices/default-lb-backend-custom-${replace(path_matcher.key, ".", "-")}"
204196
}
205197
}
206198
}
207199

208-
resource "google_certificate_manager_certificate_map" "default" {
209-
name = "${local.project_name}-cert-map"
200+
# Wildcard certs and cert map are managed by skiff2.
201+
# Per-project infra only handles custom domain certs.
202+
#
203+
data "google_certificate_manager_certificate_map" "default" {
204+
name = "${local.project_name}-wildcard-cert-map"
210205
project = var.project_id
211206
}
212207

213-
resource "google_certificate_manager_certificate" "default" {
214-
for_each = toset(local.all_domains)
215-
name = "${local.project_name}-cert-${replace(each.value, ".", "-")}"
208+
# Individual certs for custom domains (HTTP/LB authorization — no DNS needed)
209+
resource "google_certificate_manager_certificate" "custom" {
210+
for_each = var.custom_domain_mappings
211+
name = "${local.project_name}-cert-${replace(each.key, ".", "-")}"
216212
project = var.project_id
217213

218214
managed {
219-
domains = [each.value]
215+
domains = [each.key]
220216
}
221217
}
222218

223-
resource "google_certificate_manager_certificate_map_entry" "default" {
224-
for_each = toset(local.all_domains)
225-
name = "${local.project_name}-entry-${replace(each.value, ".", "-")}"
219+
# Cert map entries — custom domains only (wildcard entries managed by skiff2)
220+
resource "google_certificate_manager_certificate_map_entry" "custom" {
221+
for_each = var.custom_domain_mappings
222+
name = "${local.project_name}-entry-${replace(each.key, ".", "-")}"
226223
project = var.project_id
227-
map = google_certificate_manager_certificate_map.default.name
228-
certificates = [google_certificate_manager_certificate.default[each.value].id]
229-
hostname = each.value
224+
map = data.google_certificate_manager_certificate_map.default.name
225+
certificates = [google_certificate_manager_certificate.custom[each.key].id]
226+
hostname = each.key
230227
}
231228

229+
230+
# Load Balancer
231+
#
232232
module "lb-http" {
233233
source = "GoogleCloudPlatform/lb-http/google//modules/serverless_negs"
234234
version = "~> 12.0"
@@ -242,15 +242,14 @@ module "lb-http" {
242242
address = data.google_compute_global_address.lb_ip.address
243243

244244
ssl = true
245-
certificate_map = google_certificate_manager_certificate_map.default.id
245+
certificate_map = data.google_certificate_manager_certificate_map.default.id
246246
https_redirect = true
247247

248-
249248
create_url_map = false
250249
url_map = google_compute_url_map.default.self_link
251250

252251
backends = {
253-
for key, svc in var.services : (key == var.default_service ? "default" : key) => {
252+
for key, neg_id in local.all_backends : key => {
254253
protocol = "HTTPS"
255254
enable_cdn = false
256255
security_policy = data.google_compute_security_policy.cloud_armor.self_link
@@ -260,11 +259,7 @@ module "lb-http" {
260259
sample_rate = 1.0
261260
}
262261

263-
groups = [
264-
{
265-
group = google_compute_region_network_endpoint_group.default[key].id
266-
}
267-
]
262+
groups = [{ group = neg_id }]
268263

269264
iap_config = {
270265
enable = false

project-terraform/infra/variables.tf

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,23 @@ variable "region" {
99
default = "us-west1"
1010
}
1111

12-
variable "services" {
13-
description = "Map of Cloud Run services (all environments). Used for routing configuration."
14-
type = map(object({
15-
name = string
16-
container_name = string
17-
secondary_container_name = optional(string)
18-
allow_unauthenticated = bool
19-
allow_delete = bool
20-
secret_files = map(string)
21-
custom_domains = list(string)
22-
image_tag = string
23-
deployment_environment = string
24-
}))
25-
}
26-
2712
variable "default_service" {
28-
description = "Key from the services map to use as the default load balancer backend"
13+
description = "Cloud Run service name for the default/root service (e.g., 'ui')"
2914
type = string
3015
}
3116

3217
variable "use_classic_load_balancer" {
3318
type = bool
3419
}
20+
21+
variable "branch_environments" {
22+
description = "List of active non-prod branch environment names (sanitized)"
23+
type = list(string)
24+
default = []
25+
}
26+
27+
variable "custom_domain_mappings" {
28+
description = "Map of custom domain to Cloud Run service name"
29+
type = map(string)
30+
default = {}
31+
}

project-terraform/services/modules/cloud_run_service/main.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
# Fetch Secret Manager secrets for this service using the service name as a prefix.
23
# Filter is a substring match, so "name:my-service-" matches any secret whose name contains that string.
34
data "google_secret_manager_secrets" "app_secrets" {

shared-actions/build/dynamic-build.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -405,14 +405,6 @@ export async function main() {
405405
const rawConfig = JSON.parse(configContent);
406406
const config = BuildConfigSchema.parse(rawConfig);
407407

408-
const environments = config.environments ?? ["main"];
409-
if (shouldPush && !environments.includes(branchName)) {
410-
core.info(
411-
`Branch "${branchName}" is not in the configured environments [${environments.join(", ")}]. Skipping build.`,
412-
);
413-
return;
414-
}
415-
416408
const context: BuildContext = {
417409
registry,
418410
projectId,

0 commit comments

Comments
 (0)