Skip to content

Commit

Permalink
Add folder factory to project-factory module (#2152)
Browse files Browse the repository at this point in the history
* WIP Folder Factory

* parent keys and general fixes

* changes

* update README and example test, add support for hierarchy projects

---------

Co-authored-by: Ludo <[email protected]>
  • Loading branch information
juliocc and ludoo authored Mar 14, 2024
1 parent 93e9909 commit 28f0268
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 321 deletions.
104 changes: 88 additions & 16 deletions modules/project-factory/README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,57 @@
# Project Factory
# Project and Folder Factory

This module implements in code the end-to-end project creation process for multiple projects via YAML data configurations.
This module implements end-to-end creation processes for a folder hierarchy, projects and billing budgets via YAML data configurations.

It supports

- filesystem-driven folder hierarchy exposing the full configuration options available in the [folder module](../folder/)
- multiple project creation and management exposing the full configuration options available in the [project module](../project/), including KMS key grants and VPC-SC perimeter membership
- optional per-project [service account management](#service-accounts) including basic IAM grants
- optional [billing budgets](#billing-budgets) factory and budget/project associations
- cross-referencing of hierarchy folders in projects
- optional per-project IaC configuration (TODO)

The factory is implemented as a thin wrapping layer, so that no "magic" or hidden side effects are implemented in code, and debugging or integration of new features are simple.
The factory is implemented as a thin data translation layer for the underlying modules, so that no "magic" or hidden side effects are implemented in code, and debugging or integration of new features are simple.

The code is meant to be executed by a high level service accounts with powerful permissions:

- Shared VPC connection if service project attachment is desired
- forlder admin permissions for the hierarchy
- project creation on the nodes (folder or org) where projects will be defined
- Shared VPC connection if service project attachment is desired
- billing cost manager permissions to manage budgets and monitoring permissions if notifications should also be managed here

<!-- BEGIN TOC -->
- [Leveraging data defaults, merges, optionals](#leveraging-data-defaults-merges-optionals)
- [Additional resources](#additional-resources)
- [Folder hierarchy](#folder-hierarchy)
- [Projects](#projects)
- [Leveraging project defaults, merges, optionals](#leveraging-project-defaults-merges-optionals)
- [Service accounts](#service-accounts)
- [Billing budgets](#billing-budgets)
- [Billing budgets](#billing-budgets)
- [Example](#example)
- [Variables](#variables)
- [Outputs](#outputs)
- [Tests](#tests)
<!-- END TOC -->

## Leveraging data defaults, merges, optionals
## Folder hierarchy

The hierarchy supports up to three levels of folders, which are defined via filesystem directories each including a `_config.yaml` files detailing their attributes.

The hierarchy factory is configured via the `factories_config.hierarchy` variable via one mandatory and one optional argument:

- `factories_config.hierarchy.folders_data_path` is required to enable the hierarchy factory, and must be set to the path containing the YAML definitions
- `factories_config.hierarchy.parent_ids` is an optional map where keys are arbitrary and values are set to resource node ids

Top-level folders in the filesystem hierarchy have no explicit parent, so their parent ids need to be provided in the YAML by either referencing the full id (e.g. `folders/12345678`) or by referencing a key in the `parent_ids` attribute described above. As a shortcut, a `default` key can be defined whose value is used for any top-level folder which does not directly provide a parent id.

Filesystem directories can also contain project definitions in the same YAML format described below. This approach must be used with caution and is best adopted for stable scenarios, as problems in the filesystem hierarchy definitions might result in the project files not being read and the resources being deleted by Terraform.

Refer to the [example](#example) below for actual examples of the YAML definitions.

## Projects

The project factory is configured via the `factories_config.projects_data_path` variable, and project files are also read from the hierarchy describe in the previous section when enabled. The YAML format mirrors the project module, refer to the [example](#example) below for actual examples of the YAML definitions.

### Leveraging project defaults, merges, optionals

In addition to the YAML-based project configurations, the factory accepts three additional sets of inputs via Terraform variables:

Expand All @@ -37,8 +61,6 @@ In addition to the YAML-based project configurations, the factory accepts three

Some examples on where to use each of the three sets are [provided below](#example).

## Additional resources

### Service accounts

Service accounts can be managed as part of each project's YAML configuration. This allows creation of default service accounts used for GCE instances, in firewall rules, or for application-level credentials without resorting to a separate Terraform configuration.
Expand All @@ -59,9 +81,9 @@ service_accounts:
Both the `display_name` and `iam_self_roles` attributes are optional.

### Billing budgets
## Billing budgets

The project factory integrates the billing budgets factory exposed by the `[`billing-account`](../billing-account/) module, and adds support for easy referencing budgets in project files.
The billing budgets factory integrates the `[`billing-account`](../billing-account/) module functionality, and adds support for easy referencing budgets in project files.

To enable support for billing budgets, set the billing account id, optional notification channels, and the data folder for budgets in the `factories_config.budgets` variable, then create billing budgets using YAML definitions following the format described in the `billing-account` module.

Expand All @@ -84,6 +106,8 @@ The example below shows how to use the billing budgets factory.

## Example

The module invocation using all optional features:

```hcl
module "project-factory" {
source = "./fabric/modules/project-factory"
Expand Down Expand Up @@ -122,12 +146,56 @@ module "project-factory" {
}
}
}
hierarchy = {
folders_data_path = "data/hierarchy"
parent_ids = {
default = "folders/12345678"
}
}
projects_data_path = "data/projects"
}
}
# tftest modules=8 resources=37 files=prj-app-1,prj-app-2,prj-app-3,budget-test-100
# tftest modules=13 resources=48 files=prj-app-1,prj-app-2,prj-app-3,budget-test-100,h-0-0,h-1-0,h-0-1,h-1-1,h-1-1-p0 inventory=example.yaml
```

A simple hierarchy of folders:

```yaml
name: Foo (level 1)
iam:
roles/viewer:
- group:[email protected]
# tftest-file id=h-0-0 path=data/hierarchy/foo/_config.yaml
```

```yaml
name: Bar (level 1)
parent: folders/4567890
# tftest-file id=h-1-0 path=data/hierarchy/bar/_config.yaml
```

```yaml
name: Foo Baz (level 2)
# tftest-file id=h-0-1 path=data/hierarchy/foo/baz/_config.yaml
```

```yaml
name: Bar Baz (level 2)
# tftest-file id=h-1-1 path=data/hierarchy/bar/baz/_config.yaml
```

One project defined within the folder hierarchy:

```yaml
billing_account: 012345-67890A-BCDEF0
services:
- container.googleapis.com
- storage.googleapis.com
# tftest-file id=h-1-1-p0 path=data/hierarchy/bar/baz/bar-baz-0.yaml
```

More traditional project definitions via the project factory data:

```yaml
# project app-1
billing_account: 012345-67890A-BCDEF0
Expand Down Expand Up @@ -206,6 +274,8 @@ services:
# tftest-file id=prj-app-3 path=data/projects/prj-app-3.yaml
```

And a billing budget:

```yaml
# billing budget test-100
display_name: 100 dollars in current spend
Expand All @@ -226,12 +296,13 @@ update_rules:
- billing-default
# tftest-file id=budget-test-100 path=data/budgets/test-100.yaml
```

<!-- BEGIN TFDOC -->
## Variables

| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [factories_config](variables.tf#L91) | Path to folder with YAML resource description data files. | <code title="object&#40;&#123;&#10; projects_data_path &#61; string&#10; budgets &#61; optional&#40;object&#40;&#123;&#10; billing_account &#61; string&#10; budgets_data_path &#61; string&#10; notification_channels &#61; optional&#40;map&#40;any&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [factories_config](variables.tf#L91) | Path to folder with YAML resource description data files. | <code title="object&#40;&#123;&#10; hierarchy &#61; optional&#40;object&#40;&#123;&#10; folders_data_path &#61; string&#10; parent_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;&#10; projects_data_path &#61; optional&#40;string&#41;&#10; budgets &#61; optional&#40;object&#40;&#123;&#10; billing_account &#61; string&#10; budgets_data_path &#61; string&#10; notification_channels &#61; optional&#40;map&#40;any&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [data_defaults](variables.tf#L17) | Optional default values used when corresponding project data from files are missing. | <code title="object&#40;&#123;&#10; billing_account &#61; optional&#40;string&#41;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; metric_scopes &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; parent &#61; optional&#40;string&#41;&#10; prefix &#61; optional&#40;string&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_perimeter_bridges &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; service_perimeter_standard &#61; optional&#40;string&#41;&#10; services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; shared_vpc_service_config &#61; optional&#40;object&#40;&#123;&#10; host_project &#61; string&#10; network_users &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; service_identity_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_identity_subnet_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_iam_grants &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; network_subnet_users &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; &#125;&#41;, &#123; host_project &#61; null &#125;&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [data_merges](variables.tf#L49) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | <code title="object&#40;&#123;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; metric_scopes &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_perimeter_bridges &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [data_overrides](variables.tf#L69) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | <code title="object&#40;&#123;&#10; billing_account &#61; optional&#40;string&#41;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#41;&#10; parent &#61; optional&#40;string&#41;&#10; prefix &#61; optional&#40;string&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#41;&#10; service_perimeter_bridges &#61; optional&#40;list&#40;string&#41;&#41;&#10; service_perimeter_standard &#61; optional&#40;string&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
Expand All @@ -240,8 +311,9 @@ update_rules:

| name | description | sensitive |
|---|---|:---:|
| [projects](outputs.tf#L17) | Project module outputs. | |
| [service_accounts](outputs.tf#L22) | Service account emails. | |
| [folders](outputs.tf#L17) | Folder ids. | |
| [projects](outputs.tf#L22) | Project module outputs. | |
| [service_accounts](outputs.tf#L27) | Service account emails. | |
<!-- END TFDOC -->
## Tests

Expand Down
103 changes: 103 additions & 0 deletions modules/project-factory/factory-folders.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

locals {
_folders_path = try(
pathexpand(var.factories_config.hierarchy.folders_data_path), null
)
_folders = {
for f in local._hierarchy_files : dirname(f) => yamldecode(file(
"${coalesce(var.factories_config.hierarchy.folders_data_path, "-")}/${f}"
))
}
_hierarchy_files = try(
fileset(local._folders_path, "**/_config.yaml"),
[]
)
folders = {
for key, data in local._folders : key => merge(data, {
key = key
level = length(split("/", key))
parent_key = dirname(key)
})
}
hierarchy = merge(
try(var.factories_config.hierarchy.parent_ids, {}),
{ for k, v in module.hierarchy-folder-lvl-1 : k => v.id },
{ for k, v in module.hierarchy-folder-lvl-2 : k => v.id },
{ for k, v in module.hierarchy-folder-lvl-3 : k => v.id },
)
}

check "hierarchy-data" {
assert {
condition = (
var.factories_config.hierarchy == null ||
try(var.factories_config.hierarchy.parent_ids.default, null) != null
)
error_message = "No default set for hierarchy parent ids."
}
}

module "hierarchy-folder-lvl-1" {
source = "../folder"
for_each = { for k, v in local.folders : k => v if v.level == 1 }
parent = try(
# allow the YAML data to set the parent for this level
lookup(
var.factories_config.hierarchy.parent_ids,
each.value.parent,
# use the value as is if it's not in the parents map
each.value.parent
),
# use the default value in the initial parents map
var.factories_config.hierarchy.parent_ids.default
# fail if we don't have an explicit parent
)
name = each.value.name
iam = lookup(each.value, "iam", {})
iam_bindings = lookup(each.value, "iam_bindings", {})
iam_bindings_additive = lookup(each.value, "iam_bindings_additive", {})
iam_by_principals = lookup(each.value, "iam_by_principals", {})
org_policies = lookup(each.value, "org_policies", {})
tag_bindings = lookup(each.value, "tag_bindings", {})
}

module "hierarchy-folder-lvl-2" {
source = "../folder"
for_each = { for k, v in local.folders : k => v if v.level == 2 }
parent = module.hierarchy-folder-lvl-1[each.value.parent_key].id
name = each.value.name
iam = lookup(each.value, "iam", {})
iam_bindings = lookup(each.value, "iam_bindings", {})
iam_bindings_additive = lookup(each.value, "iam_bindings_additive", {})
iam_by_principals = lookup(each.value, "iam_by_principals", {})
org_policies = lookup(each.value, "org_policies", {})
tag_bindings = lookup(each.value, "tag_bindings", {})
}

module "hierarchy-folder-lvl-3" {
source = "../folder"
for_each = { for k, v in local.folders : k => v if v.level == 3 }
parent = module.hierarchy-folder-lvl-2[each.value.parent_key].id
name = each.value.name
iam = lookup(each.value, "iam", {})
iam_bindings = lookup(each.value, "iam_bindings", {})
iam_bindings_additive = lookup(each.value, "iam_bindings_additive", {})
iam_by_principals = lookup(each.value, "iam_by_principals", {})
org_policies = lookup(each.value, "org_policies", {})
tag_bindings = lookup(each.value, "tag_bindings", {})
}
15 changes: 13 additions & 2 deletions modules/project-factory/factory-projects.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,23 @@
* limitations under the License.
*/
locals {
_hierarchy_projects = (
{
for f in try(fileset(local._folders_path, "**/*.yaml"), []) :
basename(trimsuffix(f, ".yaml")) => merge(
{ parent = dirname(f) },
yamldecode(file("${local._folders_path}/${f}"))
)
if !endswith(f, "/_config.yaml")
}
)
_project_path = try(pathexpand(var.factories_config.projects_data_path), null)
_projects = (
_projects = merge(
{
for f in try(fileset(local._project_path, "**/*.yaml"), []) :
trimsuffix(f, ".yaml") => yamldecode(file("${local._project_path}/${f}"))
}
},
local._hierarchy_projects
)
_project_budgets = flatten([
for k, v in local._projects : [
Expand Down
12 changes: 7 additions & 5 deletions modules/project-factory/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
*/

module "projects" {
source = "../project"
for_each = local.projects
billing_account = each.value.billing_account
name = each.key
parent = try(each.value.parent, null)
source = "../project"
for_each = local.projects
billing_account = each.value.billing_account
name = each.key
parent = try(
lookup(local.hierarchy, each.value.parent, each.value.parent), null
)
prefix = each.value.prefix
auto_create_network = try(each.value.auto_create_network, false)
compute_metadata = try(each.value.compute_metadata, {})
Expand Down
5 changes: 5 additions & 0 deletions modules/project-factory/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
* limitations under the License.
*/

output "folders" {
description = "Folder ids."
value = local.folders
}

output "projects" {
description = "Project module outputs."
value = module.projects
Expand Down
Loading

0 comments on commit 28f0268

Please sign in to comment.