Skip to content

Commit 407e559

Browse files
ludookarpok78
authored andcommitted
FAST project templates example (GoogleCloudPlatform#2897)
* wip * project factory providers * working example * copyright, tfdoc * rewording * rewording * tfdoc * tfdoc * tfdoc again * fix tests * tests
1 parent 27e6f9a commit 407e559

File tree

14 files changed

+412
-22
lines changed

14 files changed

+412
-22
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Artifact Registry APT Remote Registries
2+
3+
This simple setup allows creating and configuring remote APT repositories, that can be used for instance package updates without the need for an Internet connection.
4+
5+
## Prerequisites
6+
7+
The [`project.yaml`](./project.yaml) file describes the project-level configuration needed in terms of API activation and IAM bindings.
8+
9+
If you are deploying this inside a FAST-enabled organization, the file can be lightly edited to match your configuration, and then used directly in the [project factory](../../stages/2-project-factory/).
10+
11+
This Terraform can of course be deployed using any pre-existing project. In that case use the YAML file to determine the configuration you need to set on the project:
12+
13+
- enable the APIs listed under `services`
14+
- grant the permissions listed under `iam` to the principal running Terraform, either machine (service account) or human
15+
16+
## VPC-SC Integration
17+
18+
Access to upstream sources from inside a VPC-SC service perimeter [requires specific activation](https://cloud.google.com/artifact-registry/docs/repositories/remote-repo#vpc), which depends on a high-level IAM role on the VPC-SC policy.
19+
20+
Granting such a role to the identity running this setup (either machine or human) is not realistic, so the choice made here is to output the relevant command, so that a VPC-SC administrator can run it using the appropriate credentials. The [relevant Terraform resource](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/artifact_registry_vpcsc_config) can of course be used to automate this task when needed.
21+
22+
## Instance-level Access to the Repository
23+
24+
Instances that need access to the created registries require the `roles/artifactregistry.writer` role assigned to the instance service accounts. This can be automated via the `apt_remote_registries` variable described below, to create IAm bindings for each registry.
25+
26+
It's also possible (and maybe desirable) to grant the role at the project level, if access to multiple repositories is needed from the same set of principals. This needs of course to happen where the project is managed, for example in the project factory YAML file.
27+
28+
Once proper access has been configured, the `apt_configs` output can be used as a basis to configure the APT sources lists on each instance.
29+
30+
Instance need to have the `apt-transport-artifact-registry` package installed, which is served by the default internal repositories configured on GCE base images.
31+
32+
```bash
33+
sudo apt install apt-transport-artifact-registry
34+
```
35+
36+
## Variable Configuration
37+
38+
This is an example of running this stage. Note that the `apt_remote_registries` has a default value that can be used when no IAM is needed at the registry level, and the default set of remotes is fine.
39+
40+
```hcl
41+
project_id = "my-project"
42+
location = "europe-west3"
43+
apt_remote_registries = [
44+
{ path = "DEBIAN debian/dists/bookworm" },
45+
{
46+
path = "DEBIAN debian-security/dists/bookworm-security"
47+
# grant specific access permissions to this registry
48+
writer_principals = [
49+
"serviceAccount:[email protected]"
50+
]
51+
}
52+
]
53+
# tftest skip
54+
```
55+
<!-- BEGIN TFDOC -->
56+
## Variables
57+
58+
| name | description | type | required | default |
59+
|---|---|:---:|:---:|:---:|
60+
| [project_id](variables.tf#L56) | Project id where the registries will be created. | <code>string</code> || |
61+
| [apt_remote_registries](variables.tf#L17) | Remote artifact registry configurations. | <code title="list&#40;object&#40;&#123;&#10; path &#61; string&#10; writer_principals &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10;&#125;&#41;&#41;">list&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code title="&#91;&#10; &#123; path &#61; &#34;DEBIAN debian&#47;dists&#47;bookworm&#34; &#125;,&#10; &#123; path &#61; &#34;DEBIAN debian-security&#47;dists&#47;bookworm-security&#34; &#125;&#10;&#93;">&#91;&#8230;&#93;</code> |
62+
| [location](variables.tf#L43) | Region where the registries will be created. | <code>string</code> | | <code>&#34;europe-west8&#34;</code> |
63+
| [name](variables.tf#L49) | Prefix used for all resource names. | <code>string</code> | | <code>&#34;apt-remote&#34;</code> |
64+
65+
## Outputs
66+
67+
| name | description | sensitive |
68+
|---|---|:---:|
69+
| [apt_configs](outputs.tf#L23) | APT configurations for remote registries. | |
70+
| [vpcsc_command](outputs.tf#L33) | Command to allow egress to remotes from inside a perimeter. | |
71+
<!-- END TFDOC -->
72+
73+
## Test
74+
75+
```hcl
76+
module "test" {
77+
source = "./fabric/fast/project-templates/os-apt-registries"
78+
project_id = "my-project"
79+
location = "europe-west3"
80+
apt_remote_registries = [
81+
{ path = "DEBIAN debian/dists/bookworm" },
82+
{
83+
path = "DEBIAN debian-security/dists/bookworm-security"
84+
# grant specific access permissions to this registry
85+
writer_principals = [
86+
"serviceAccount:[email protected]"
87+
]
88+
}
89+
]
90+
}
91+
# tftest modules=3 resources=4
92+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
locals {
18+
apt_remote_registries = {
19+
for v in var.apt_remote_registries : (v.path) => merge(v, {
20+
name = element(split("/", split(" ", v.path)[1]), -1)
21+
})
22+
}
23+
}
24+
25+
module "registries" {
26+
source = "../../../modules/artifact-registry"
27+
for_each = local.apt_remote_registries
28+
project_id = var.project_id
29+
location = var.location
30+
name = "${var.name}-${each.value.name}"
31+
format = {
32+
apt = {
33+
remote = {
34+
public_repository = each.value.path
35+
}
36+
}
37+
}
38+
iam = {
39+
"roles/artifactregistry.writer" = each.value.writer_principals
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
locals {
18+
format = "deb ar+https://%s-apt.pkg.dev/remote/%s/%s %s main"
19+
}
20+
21+
# europe-west8-apt.pkg.dev/ldj-prod-os-apt-0/apt-remote-bookworm
22+
23+
output "apt_configs" {
24+
description = "APT configurations for remote registries."
25+
value = {
26+
for k, v in module.registries : v.name => format(
27+
local.format, var.location, var.project_id,
28+
v.name, local.apt_remote_registries[k].name
29+
)
30+
}
31+
}
32+
33+
output "vpcsc_command" {
34+
description = "Command to allow egress to remotes from inside a perimeter."
35+
value = (
36+
"gcloud artifacts vpcsc-config allow --project=${var.project_id} --location=${var.location}"
37+
)
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# yaml-language-server: $schema=../../stages/2-project-factory/schemas/project.schema.json
16+
17+
# TODO: edit and uncomment the following line to create the project in a folder
18+
# parent: shared
19+
20+
name: os-apt-0
21+
services:
22+
- accesscontextmanager.googleapis.com
23+
- artifactregistry.googleapis.com
24+
25+
automation:
26+
# TODO: edit the automation project and optionally edit resource names
27+
project: pf-automation-0
28+
service_accounts:
29+
rw:
30+
description: Read/write automation service account for apt registries.
31+
buckets:
32+
tf-state:
33+
description: Terraform state bucket for apt registries.
34+
iam:
35+
roles/storage.objectCreator:
36+
- rw
37+
roles/storage.objectViewer:
38+
- rw
39+
iam:
40+
roles/viewer:
41+
- rw
42+
roles/artifactregistry.admin:
43+
- rw
44+
# TODO: add instance service accounts that need access to the registries
45+
# roles/artifactregistry.writer:
46+
# - serviceAccount:foo@bar
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
variable "apt_remote_registries" {
18+
description = "Remote artifact registry configurations."
19+
type = list(object({
20+
path = string
21+
writer_principals = optional(list(string), [])
22+
}))
23+
nullable = false
24+
default = [
25+
{ path = "DEBIAN debian/dists/bookworm" },
26+
{ path = "DEBIAN debian-security/dists/bookworm-security" }
27+
]
28+
validation {
29+
condition = alltrue([
30+
for v in var.apt_remote_registries : length(split(" ", v.path)) == 2
31+
])
32+
error_message = "Invalid registry path: format is [BASE] [path]."
33+
}
34+
validation {
35+
condition = alltrue([
36+
for v in var.apt_remote_registries :
37+
contains(["DEBIAN", "UBUNTU"], element(split(" ", v.path), 0))
38+
])
39+
error_message = "Invalid registry base: only 'DEBIAN' and 'UBUNTU' are supported."
40+
}
41+
}
42+
43+
variable "location" {
44+
description = "Region where the registries will be created."
45+
type = string
46+
default = "europe-west8"
47+
}
48+
49+
variable "name" {
50+
description = "Prefix used for all resource names."
51+
type = string
52+
nullable = true
53+
default = "apt-remote"
54+
}
55+
56+
variable "project_id" {
57+
description = "Project id where the registries will be created."
58+
type = string
59+
}

fast/stages/2-project-factory/README.md

+19-16
Original file line numberDiff line numberDiff line change
@@ -342,31 +342,34 @@ The approach is not shown here but reasonably easy to implement. The main projec
342342
<!-- BEGIN TFDOC -->
343343
## Files
344344
345-
| name | description | modules |
346-
|---|---|---|
347-
| [main.tf](./main.tf) | Project factory. | <code>project-factory</code> |
348-
| [outputs.tf](./outputs.tf) | Module outputs. | |
349-
| [variables-fast.tf](./variables-fast.tf) | None | |
350-
| [variables.tf](./variables.tf) | Module variables. | |
345+
| name | description | modules | resources |
346+
|---|---|---|---|
347+
| [main.tf](./main.tf) | Project factory. | <code>project-factory</code> | |
348+
| [outputs.tf](./outputs.tf) | Module outputs. | | <code>google_storage_bucket_object</code> · <code>local_file</code> |
349+
| [variables-fast.tf](./variables-fast.tf) | None | | |
350+
| [variables.tf](./variables.tf) | Module variables. | | |
351351
352352
## Variables
353353
354354
| name | description | type | required | default | producer |
355355
|---|---|:---:|:---:|:---:|:---:|
356-
| [billing_account](variables-fast.tf#L17) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | <code title="object&#40;&#123;&#10; id &#61; string&#10; is_org_level &#61; optional&#40;bool, true&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | <code>0-bootstrap</code> |
357-
| [prefix](variables-fast.tf#L65) | Prefix used for resources that need unique names. Use a maximum of 9 chars for organizations, and 11 chars for tenants. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
356+
| [automation](variables-fast.tf#L17) | Automation resources created by the bootstrap stage. | <code title="object&#40;&#123;&#10; outputs_bucket &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | <code>0-bootstrap</code> |
357+
| [billing_account](variables-fast.tf#L26) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | <code title="object&#40;&#123;&#10; id &#61; string&#10; is_org_level &#61; optional&#40;bool, true&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | <code>0-bootstrap</code> |
358+
| [prefix](variables-fast.tf#L74) | Prefix used for resources that need unique names. Use a maximum of 9 chars for organizations, and 11 chars for tenants. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
358359
| [factories_config](variables.tf#L17) | Configuration for YAML-based factories. | <code title="object&#40;&#123;&#10; folders_data_path &#61; optional&#40;string, &#34;data&#47;hierarchy&#34;&#41;&#10; projects_data_path &#61; optional&#40;string, &#34;data&#47;projects&#34;&#41;&#10; budgets &#61; optional&#40;object&#40;&#123;&#10; billing_account &#61; string&#10; budgets_data_path &#61; optional&#40;string, &#34;data&#47;budgets&#34;&#41;&#10; notification_channels &#61; optional&#40;map&#40;any&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;&#10; context &#61; optional&#40;object&#40;&#123;&#10; folder_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; iam_principals &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; tag_values &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; vpc_host_projects &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
359-
| [folder_ids](variables-fast.tf#L30) | Folders created in the resource management stage. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>1-resman</code> |
360-
| [groups](variables-fast.tf#L38) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>0-bootstrap</code> |
361-
| [host_project_ids](variables-fast.tf#L47) | Host project for the shared VPC. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>2-networking</code> |
362-
| [locations](variables-fast.tf#L55) | Optional locations for GCS, BigQuery, and logging buckets created here. | <code title="object&#40;&#123;&#10; gcs &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | <code>0-bootstrap</code> |
363-
| [service_accounts](variables-fast.tf#L75) | Automation service accounts in name => email format. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>1-resman</code> |
364-
| [tag_values](variables-fast.tf#L83) | FAST-managed resource manager tag values. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>1-resman</code> |
360+
| [folder_ids](variables-fast.tf#L39) | Folders created in the resource management stage. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>1-resman</code> |
361+
| [groups](variables-fast.tf#L47) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>0-bootstrap</code> |
362+
| [host_project_ids](variables-fast.tf#L56) | Host project for the shared VPC. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>2-networking</code> |
363+
| [locations](variables-fast.tf#L64) | Optional locations for GCS, BigQuery, and logging buckets created here. | <code title="object&#40;&#123;&#10; gcs &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | <code>0-bootstrap</code> |
364+
| [outputs_location](variables.tf#L39) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | <code>string</code> | | <code>null</code> | |
365+
| [service_accounts](variables-fast.tf#L84) | Automation service accounts in name => email format. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>1-resman</code> |
366+
| [stage_name](variables.tf#L45) | FAST stage name. Used to separate output files across different factories. | <code>string</code> | | <code>&#34;2-project-factory&#34;</code> | |
367+
| [tag_values](variables-fast.tf#L92) | FAST-managed resource manager tag values. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>1-resman</code> |
365368

366369
## Outputs
367370

368371
| name | description | sensitive | consumers |
369372
|---|---|:---:|---|
370-
| [projects](outputs.tf#L17) | Created projects. | | |
371-
| [service_accounts](outputs.tf#L22) | Created service accounts. | | |
373+
| [projects](outputs.tf#L32) | Created projects. | | |
374+
| [service_accounts](outputs.tf#L46) | Created service accounts. | | |
372375
<!-- END TFDOC -->

fast/stages/2-project-factory/outputs.tf

+53-2
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,63 @@
1414
* limitations under the License.
1515
*/
1616

17+
locals {
18+
project_outputs = {
19+
for k, v in module.projects.projects : k => {
20+
bucket = try(
21+
v.automation_buckets["state"],
22+
v.automation_buckets["tf-state"],
23+
null
24+
)
25+
project_id = v.project_id
26+
sa = try(v.automation_service_accounts["rw"], null)
27+
sa_ro = try(v.automation_service_accounts["ro"], null)
28+
} if v.automation_enabled
29+
}
30+
}
31+
1732
output "projects" {
1833
description = "Created projects."
19-
value = module.projects.projects
34+
value = {
35+
for k, v in module.projects.projects : k => {
36+
id = v.project_id
37+
number = v.number
38+
automation = {
39+
buckets = v.automation_buckets
40+
service_accounts = v.automation_service_accounts
41+
}
42+
}
43+
}
2044
}
2145

2246
output "service_accounts" {
2347
description = "Created service accounts."
24-
value = module.projects.service_accounts
48+
value = {
49+
for k, v in module.projects.service_accounts : k => {
50+
email = v.email
51+
iam_emanil = v.iam_email
52+
}
53+
}
54+
}
55+
56+
# generate tfvars file for subsequent stages
57+
58+
resource "local_file" "providers" {
59+
for_each = var.outputs_location == null ? {} : {
60+
for k, v in local.project_outputs : k => v
61+
if v.bucket != null && v.sa != null
62+
}
63+
file_permission = "0644"
64+
filename = "${try(pathexpand(var.outputs_location), "")}/providers/${var.stage_name}/${each.key}-providers.tf"
65+
content = templatefile("templates/providers.tf.tpl", each.value)
66+
}
67+
68+
resource "google_storage_bucket_object" "tfvars" {
69+
for_each = var.outputs_location == null ? {} : {
70+
for k, v in local.project_outputs : k => v
71+
if v.bucket != null && v.sa != null
72+
}
73+
bucket = var.automation.outputs_bucket
74+
name = "providers/${var.stage_name}/${each.key}-providers.tf"
75+
content = templatefile("templates/providers.tf.tpl", each.value)
2576
}

0 commit comments

Comments
 (0)