IAM interface refactor (#1595)

* IAM modules refactor proposal

* policy

* subheading

* Update 20230816-iam-refactor.md

* log Julio's +1

* data-catalog-policy-tag

* dataproc

* dataproc

* folder

* folder

* folder

* folder

* project

* better filtering in test examples

* project

* folder

* folder

* organization

* fix variable descriptions

* kms

* net-vpc

* dataplex-datascan

* modules/iam-service-account

* modules/source-repository/

* blueprints/cloud-operations/vm-migration/

* blueprints/third-party-solutions/wordpress

* dataplex-datascan

* blueprints/cloud-operations/workload-identity-federation

* blueprints/data-solutions/cloudsql-multiregion/

* blueprints/data-solutions/composer-2

* Update 20230816-iam-refactor.md

* Update 20230816-iam-refactor.md

* capture discussion in architectural doc

* update variable names and refactor proposal

* project

* blueprints first round

* folder

* organization

* data-catalog-policy-tag

* re-enable folder inventory

* project module style fix

* dataproc

* source-repository

* source-repository tests

* dataplex-datascan

* dataplex-datascan tests

* net-vpc

* net-vpc test examples

* iam-service-account

* iam-service-account test examples

* kms

* boilerplate

* tfdoc

* fix module tests

* more blueprint fixes

* fix typo in data blueprints

* incomplete refactor of data platform foundations

* tfdoc

* data platform foundation

* refactor data platform foundation iam locals

* remove redundant example test

* shielded folder fix

* fix typo

* project factory

* project factory outputs

* tfdoc

* test workflow: less verbose tests, fix tf version

* re-enable -vv, shorter traceback, fix action version

* ignore github extension warning, re-enable action version

* fast bootstrap IAM, untested

* bootstrap stage IAM fixes

* stage 0 tests

* fast stage 1

* tenant stage 1

* minor changes to fast stage 0 and 1

* fast security stage

* fast mt stage 0

* fast mt stage 0

* fast pf
This commit is contained in:
Ludovico Magnocavallo 2023-08-20 09:44:20 +02:00 committed by GitHub
parent 6eeba5e599
commit 819894d2ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
154 changed files with 3273 additions and 3773 deletions

View File

@ -28,7 +28,7 @@ jobs:
name: "Create tag on master if there was activity in last 24 hours"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: "Check changes and tag"
run: |

View File

@ -17,9 +17,6 @@ on:
pull_request:
branches:
- master
tags:
- ci
- lint
jobs:
linting:

View File

@ -19,9 +19,6 @@ on:
pull_request:
branches:
- master
tags:
- ci
- test
env:
GOOGLE_APPLICATION_CREDENTIALS: "/home/runner/credentials.json"
@ -39,7 +36,7 @@ jobs:
- uses: hashicorp/setup-terraform@v2
with:
terraform_version: ${{ env.TERRAFORM_VERSION }}
terraform_version: ${{ env.TF_VERSION }}
terraform_wrapper: false
- name: Build lockfile and fetch providers
@ -76,10 +73,10 @@ jobs:
uses: ./.github/actions/fabric-tests
with:
PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
TERRAFORM_VERSION: ${{ env.TERRAFORM_VERSION }}
TERRAFORM_VERSION: ${{ env.TF_VERSION }}
- name: Run tests on documentation examples
run: pytest -vv -n4 -k blueprints/ tests/examples
run: pytest -vv -n4 --tb=line -k blueprints/ tests/examples
examples-modules:
runs-on: ubuntu-latest
@ -91,10 +88,10 @@ jobs:
uses: ./.github/actions/fabric-tests
with:
PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
TERRAFORM_VERSION: ${{ env.TERRAFORM_VERSION }}
TERRAFORM_VERSION: ${{ env.TF_VERSION }}
- name: Run tests on documentation examples
run: pytest -vv -n4 -k modules/ tests/examples
run: pytest -vv -n4 --tb=line -k modules/ tests/examples
blueprints:
runs-on: ubuntu-latest
@ -106,10 +103,10 @@ jobs:
uses: ./.github/actions/fabric-tests
with:
PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
TERRAFORM_VERSION: ${{ env.TERRAFORM_VERSION }}
TERRAFORM_VERSION: ${{ env.TF_VERSION }}
- name: Run tests environments
run: pytest -vv -n4 tests/blueprints
run: pytest -vv -n4 --tb=line tests/blueprints
modules:
runs-on: ubuntu-latest
@ -121,10 +118,10 @@ jobs:
uses: ./.github/actions/fabric-tests
with:
PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
TERRAFORM_VERSION: ${{ env.TERRAFORM_VERSION }}
TERRAFORM_VERSION: ${{ env.TF_VERSION }}
- name: Run tests modules
run: pytest -vv -n4 tests/modules
run: pytest -vv -n4 --tb=line tests/modules
fast:
runs-on: ubuntu-latest
@ -136,7 +133,7 @@ jobs:
uses: ./.github/actions/fabric-tests
with:
PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
TERRAFORM_VERSION: ${{ env.TERRAFORM_VERSION }}
TERRAFORM_VERSION: ${{ env.TF_VERSION }}
- name: Run tests on FAST stages
run: pytest -vv -n4 tests/fast
run: pytest -vv -n4 --tb=line tests/fast

View File

@ -13,21 +13,20 @@ This is the high level diagram:
This sample creates\updates several distinct groups of resources:
- projects
- Deploy M4CE host project with [required services](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#enabling_required_services_on_the_host_project) on a new or existing project.
- M4CE target project prerequisites deployed on existing projects.
- Deploy M4CE host project with [required services](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#enabling_required_services_on_the_host_project) on a new or existing project.
- M4CE target project prerequisites deployed on existing projects.
- IAM
- Create a [service account](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/migrate-connector#step-3) used at runtime by the M4CE connector for data replication
- Grant [migration admin roles](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/enable-services#using_predefined_roles) to provided user accounts
- Grant [migration viewer role](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/enable-services#using_predefined_roles) to provided user accounts
- Grant [migration admin roles](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/enable-services#using_predefined_roles) to provided user or group
- Grant [migration viewer role](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/enable-services#using_predefined_roles) to provided user or group
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [migration_admin_users](variables.tf#L15) | List of users authorized to create a new M4CE sources and perform all other migration operations, in IAM format. | <code>list&#40;string&#41;</code> | ✓ | |
| [migration_admin](variables.tf#L15) | User or group who can create a new M4CE sources and perform all other migration operations, in IAM format (`group:foo@example.com`). | <code>string</code> | ✓ | |
| [migration_target_projects](variables.tf#L20) | List of target projects for m4ce workload migrations. | <code>list&#40;string&#41;</code> | ✓ | |
| [migration_viewer_users](variables.tf#L25) | List of users authorized to retrieve information about M4CE in the Google Cloud Console, in IAM format. | <code>list&#40;string&#41;</code> | | <code>&#91;&#93;</code> |
| [migration_viewer](variables.tf#L25) | User or group authorized to retrieve information about M4CE in the Google Cloud Console, in IAM format (`group:foo@example.com`). | <code>string</code> | | <code>null</code> |
| [project_create](variables.tf#L31) | Parameters for the creation of the new project to host the M4CE backend. | <code title="object&#40;&#123;&#10; billing_account_id &#61; string&#10; parent &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [project_name](variables.tf#L40) | Name of an existing project or of the new project assigned as M4CE host project. | <code>string</code> | | <code>&#34;m4ce-host-project-000&#34;</code> |
@ -36,9 +35,7 @@ This sample creates\updates several distinct groups of resources:
| name | description | sensitive |
|---|---|:---:|
| [m4ce_gmanaged_service_account](outputs.tf#L15) | Google managed service account created automatically during the migrate connector registration.. It is used by M4CE to perform activities on target projects. | |
<!-- END TFDOC -->
## Test
```hcl
@ -48,8 +45,8 @@ module "test" {
billing_account_id = "1234-ABCD-1234"
parent = "folders/1234563"
}
migration_admin_users = ["user:admin@example.com"]
migration_viewer_users = ["user:viewer@example.com"]
migration_admin = "user:admin@example.com"
migration_viewer = "user:viewer@example.com"
migration_target_projects = [module.test-target-project.name]
depends_on = [
module.test-target-project

View File

@ -19,11 +19,11 @@ module "host-project" {
: null
)
name = var.project_name
parent = (var.project_create != null
parent = (
var.project_create != null
? var.project_create.parent
: null
)
services = [
"cloudresourcemanager.googleapis.com",
"compute.googleapis.com",
@ -33,14 +33,24 @@ module "host-project" {
"servicecontrol.googleapis.com",
"vmmigration.googleapis.com",
]
project_create = var.project_create != null
iam_additive = {
"roles/iam.serviceAccountKeyAdmin" = var.migration_admin_users,
"roles/iam.serviceAccountCreator" = var.migration_admin_users,
"roles/vmmigration.admin" = var.migration_admin_users,
"roles/vmmigration.viewer" = var.migration_viewer_users,
iam_bindings_additive = {
admin_sa_key_admin = {
role = "roles/iam.serviceAccountKeyAdmin"
member = var.migration_admin
}
admin_sa_creator = {
role = "roles/iam.serviceAccountCreator"
member = var.migration_admin
}
admin_vmm_admin = {
role = "roles/vmmigration.admin"
member = var.migration_admin
}
viewer_vmm_viewer = {
role = "roles/vmmigration.viewer"
member = var.migration_viewer
}
}
}
@ -56,7 +66,6 @@ module "target-projects" {
source = "../../../../modules/project"
name = each.key
project_create = false
services = [
"servicemanagement.googleapis.com",
"servicecontrol.googleapis.com",
@ -64,10 +73,18 @@ module "target-projects" {
"cloudresourcemanager.googleapis.com",
"compute.googleapis.com"
]
iam_additive = {
"roles/resourcemanager.projectIamAdmin" = var.migration_admin_users,
"roles/compute.viewer" = var.migration_admin_users,
"roles/iam.serviceAccountUser" = var.migration_admin_users
iam_bindings_additive = {
admin_project_iam_admin = {
role = "roles/resourcemanager.projectIamAdmin"
member = var.migration_admin
}
admin_compute_viewer = {
role = "roles/compute.viewer"
member = var.migration_admin
}
admin_sa_user = {
role = "roles/iam.serviceAccountUser"
member = var.migration_admin
}
}
}

View File

@ -12,9 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
variable "migration_admin_users" {
description = "List of users authorized to create a new M4CE sources and perform all other migration operations, in IAM format."
type = list(string)
variable "migration_admin" {
description = "User or group who can create a new M4CE sources and perform all other migration operations, in IAM format (`group:foo@example.com`)."
type = string
}
variable "migration_target_projects" {
@ -22,10 +22,10 @@ variable "migration_target_projects" {
type = list(string)
}
variable "migration_viewer_users" {
description = "List of users authorized to retrieve information about M4CE in the Google Cloud Console, in IAM format."
type = list(string)
default = []
variable "migration_viewer" {
description = "User or group authorized to retrieve information about M4CE in the Google Cloud Console, in IAM format (`group:foo@example.com`)."
type = string
default = null
}
variable "project_create" {

View File

@ -13,34 +13,33 @@ This is the high level diagram:
This sample creates\update several distinct groups of resources:
- projects
- M4CE host project with [required services](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#enabling_required_services_on_the_host_project) deployed on a new or existing project.
- M4CE target project prerequisites deployed on existing projects.
- M4CE host project with [required services](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#enabling_required_services_on_the_host_project) deployed on a new or existing project.
- M4CE target project prerequisites deployed on existing projects.
- IAM
- Create a [service account](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/migrate-connector#step-3) used at runtime by the M4CE connector for data replication
- Grant [migration admin roles](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/enable-services#using_predefined_roles) to provided user accounts.
- Grant [migration viewer role](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/enable-services#using_predefined_roles) to provided user accounts.
- Grant [roles on shared VPC](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/target-project#configure-permissions) to migration admins
- Grant [migration admin roles](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/enable-services#using_predefined_roles) to provided user or group.
- Grant [migration viewer role](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/enable-services#using_predefined_roles) to provided user or group.
- Grant [roles on shared VPC](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/target-project#configure-permissions) to migration user or group
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [migration_admin_users](variables.tf#L15) | List of users authorized to create a new M4CE sources and perform all other migration operations, in IAM format. | <code>list&#40;string&#41;</code> | ✓ | |
| [migration_admin](variables.tf#L15) | User or group who can create a new M4CE sources and perform all other migration operations, in IAM format (`group:foo@example.com`). | <code>string</code> | ✓ | |
| [migration_target_projects](variables.tf#L20) | List of target projects for m4ce workload migrations. | <code>list&#40;string&#41;</code> | ✓ | |
| [sharedvpc_host_projects](variables.tf#L45) | List of host projects that share a VPC with the selected target projects. | <code>list&#40;string&#41;</code> | ✓ | |
| [migration_viewer_users](variables.tf#L25) | List of users authorized to retrieve information about M4CE in the Google Cloud Console, in IAM format. | <code>list&#40;string&#41;</code> | | <code>&#91;&#93;</code> |
| [project_create](variables.tf#L30) | Parameters for the creation of the new project to host the M4CE backend. | <code title="object&#40;&#123;&#10; billing_account_id &#61; string&#10; parent &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [project_name](variables.tf#L39) | Name of an existing project or of the new project assigned as M4CE host project. | <code>string</code> | | <code>&#34;m4ce-host-project-000&#34;</code> |
| [sharedvpc_host_projects](variables.tf#L46) | List of host projects that share a VPC with the selected target projects. | <code>list&#40;string&#41;</code> | ✓ | |
| [migration_viewer](variables.tf#L25) | User or group authorized to retrieve information about M4CE in the Google Cloud Console, in IAM format (`group:foo@example.com`). | <code>string</code> | | <code>null</code> |
| [project_create](variables.tf#L31) | Parameters for the creation of the new project to host the M4CE backend. | <code title="object&#40;&#123;&#10; billing_account_id &#61; string&#10; parent &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [project_name](variables.tf#L40) | Name of an existing project or of the new project assigned as M4CE host project. | <code>string</code> | | <code>&#34;m4ce-host-project-000&#34;</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [m4ce_gmanaged_service_account](outputs.tf#L15) | Google managed service account created automatically during the migrate connector registration. It is used by M4CE to perform activities on target projects. | |
<!-- END TFDOC -->
## Manual Steps
Once this blueprint is deployed the M4CE [m4ce_gmanaged_service_account](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/target-sa-compute-engine#configuring_the_default_service_account) has to be configured to grant the access to the shared VPC and allow the deploy of Compute Engine instances as the result of the migration.
## Test
@ -52,8 +51,8 @@ module "test" {
billing_account_id = "1234-ABCD-1234"
parent = "folders/1234563"
}
migration_admin_users = ["user:admin@example.com"]
migration_viewer_users = ["user:viewer@example.com"]
migration_admin = "user:admin@example.com"
migration_viewer = "user:viewer@example.com"
migration_target_projects = [module.test-target-project.name]
sharedvpc_host_projects = [module.test-sharedvpc-host-project.name]
depends_on = [

View File

@ -23,7 +23,6 @@ module "host-project" {
? var.project_create.parent
: null
)
services = [
"cloudresourcemanager.googleapis.com",
"compute.googleapis.com",
@ -33,14 +32,24 @@ module "host-project" {
"servicecontrol.googleapis.com",
"vmmigration.googleapis.com",
]
project_create = var.project_create != null
iam_additive = {
"roles/iam.serviceAccountKeyAdmin" = var.migration_admin_users,
"roles/iam.serviceAccountCreator" = var.migration_admin_users,
"roles/vmmigration.admin" = var.migration_admin_users,
"roles/vmmigration.viewer" = var.migration_viewer_users,
iam_bindings_additive = {
admin_sa_key_admin = {
role = "roles/iam.serviceAccountKeyAdmin"
member = var.migration_admin
}
admin_sa_creator = {
role = "roles/iam.serviceAccountCreator"
member = var.migration_admin
}
admin_vmm_admin = {
role = "roles/vmmigration.admin"
member = var.migration_admin
}
viewer_vmm_viewer = {
role = "roles/vmmigration.viewer"
member = var.migration_viewer
}
}
}
@ -51,12 +60,10 @@ module "m4ce-service-account" {
}
module "target-projects" {
for_each = toset(var.migration_target_projects)
source = "../../../../modules/project"
name = each.key
project_create = false
services = [
"cloudresourcemanager.googleapis.com",
"compute.googleapis.com",
@ -64,21 +71,27 @@ module "target-projects" {
"servicemanagement.googleapis.com",
"servicecontrol.googleapis.com",
]
iam_additive = {
"roles/resourcemanager.projectIamAdmin" = var.migration_admin_users,
"roles/iam.serviceAccountUser" = var.migration_admin_users,
iam_bindings_additive = {
admin_project_iam_admin = {
role = "roles/resourcemanager.projectIamAdmin"
member = var.migration_admin
}
admin_sa_user = {
role = "roles/iam.serviceAccountUser"
member = var.migration_admin
}
}
}
module "sharedvpc_host_project" {
for_each = toset(var.sharedvpc_host_projects)
source = "../../../../modules/project"
name = each.key
project_create = false
iam_additive = {
"roles/compute.viewer" = var.migration_admin_users,
iam_bindings_additive = {
admin_compute_viewer = {
role = "roles/compute.viewer"
member = var.migration_admin
}
}
}

View File

@ -12,9 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
variable "migration_admin_users" {
description = "List of users authorized to create a new M4CE sources and perform all other migration operations, in IAM format."
type = list(string)
variable "migration_admin" {
description = "User or group who can create a new M4CE sources and perform all other migration operations, in IAM format (`group:foo@example.com`)."
type = string
}
variable "migration_target_projects" {
@ -22,11 +22,12 @@ variable "migration_target_projects" {
type = list(string)
}
variable "migration_viewer_users" {
description = "List of users authorized to retrieve information about M4CE in the Google Cloud Console, in IAM format."
type = list(string)
default = []
variable "migration_viewer" {
description = "User or group authorized to retrieve information about M4CE in the Google Cloud Console, in IAM format (`group:foo@example.com`)."
type = string
default = null
}
variable "project_create" {
description = "Parameters for the creation of the new project to host the M4CE backend."
type = object({

View File

@ -13,21 +13,20 @@ This is the high level diagram:
This sample creates several distinct groups of resources:
- projects
- M4CE host project with [required services](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#enabling_required_services_on_the_host_project) deployed on a new or existing project.
- M4CE host project with [required services](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#enabling_required_services_on_the_host_project) deployed on a new or existing project.
- networking
- Default VPC network
- IAM
- One [service account](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/migrate-connector#step-3) used at runtime by the M4CE connector for data replication
- Grant [migration admin roles](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/enable-services#using_predefined_roles) to admin user accounts
- Grant [migration viewer role](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/enable-services#using_predefined_roles) to viewer user accounts
- Grant [migration admin roles](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/enable-services#using_predefined_roles) to admin user or group
- Grant [migration viewer role](https://cloud.google.com/migrate/virtual-machines/docs/5.0/how-to/enable-services#using_predefined_roles) to viewer user or group
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [migration_admin_users](variables.tf#L15) | List of users authorized to create a new M4CE sources and perform all other migration operations, in IAM format. | <code>list&#40;string&#41;</code> | ✓ | |
| [migration_viewer_users](variables.tf#L20) | List of users authorized to retrieve information about M4CE in the Google Cloud Console, in IAM format. | <code>list&#40;string&#41;</code> | | <code>&#91;&#93;</code> |
| [migration_admin](variables.tf#L15) | User or group who can create a new M4CE sources and perform all other migration operations, in IAM format (`group:foo@example.com`). | <code>string</code> | ✓ | |
| [migration_viewer](variables.tf#L20) | User or group authorized to retrieve information about M4CE in the Google Cloud Console, in IAM format (`group:foo@example.com`). | <code>string</code> | | <code>null</code> |
| [project_create](variables.tf#L26) | Parameters for the creation of the new project to host the M4CE backend. | <code title="object&#40;&#123;&#10; billing_account_id &#61; string&#10; parent &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [project_name](variables.tf#L35) | Name of an existing project or of the new project assigned as M4CE host an target project. | <code>string</code> | | <code>&#34;m4ce-host-project-000&#34;</code> |
| [vpc_config](variables.tf#L41) | Parameters to create a simple VPC on the M4CE project. | <code title="object&#40;&#123;&#10; ip_cidr_range &#61; string,&#10; region &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; ip_cidr_range &#61; &#34;10.200.0.0&#47;20&#34;,&#10; region &#61; &#34;us-west2&#34;&#10;&#125;">&#123;&#8230;&#125;</code> |
@ -37,9 +36,7 @@ This sample creates several distinct groups of resources:
| name | description | sensitive |
|---|---|:---:|
| [m4ce_gmanaged_service_account](outputs.tf#L15) | Google managed service account created automatically during the migrate connector registration. It is used by M4CE to perform activities on target projects. | |
<!-- END TFDOC -->
## Test
```hcl
@ -49,8 +46,8 @@ module "test" {
billing_account_id = "1234-ABCD-1234"
parent = "folders/1234563"
}
migration_admin_users = ["user:admin@example.com"]
migration_viewer_users = ["user:viewer@example.com"]
migration_admin = "user:admin@example.com"
migration_viewer = "user:viewer@example.com"
}
# tftest modules=5 resources=22
```

View File

@ -23,7 +23,6 @@ module "landing-project" {
? var.project_create.parent
: null
)
services = [
"cloudresourcemanager.googleapis.com",
"compute.googleapis.com",
@ -34,14 +33,24 @@ module "landing-project" {
"servicecontrol.googleapis.com",
"vmmigration.googleapis.com"
]
project_create = var.project_create != null
iam_additive = {
"roles/iam.serviceAccountKeyAdmin" = var.migration_admin_users,
"roles/iam.serviceAccountCreator" = var.migration_admin_users,
"roles/vmmigration.admin" = var.migration_admin_users,
"roles/vmmigration.viewer" = var.migration_viewer_users
iam_bindings_additive = {
admin_sa_key_admin = {
role = "roles/iam.serviceAccountKeyAdmin"
member = var.migration_admin
}
admin_sa_creator = {
role = "roles/iam.serviceAccountCreator"
member = var.migration_admin
}
admin_vmm_admin = {
role = "roles/vmmigration.admin"
member = var.migration_admin
}
viewer_vmm_viewer = {
role = "roles/vmmigration.viewer"
member = var.migration_viewer
}
}
}

View File

@ -12,15 +12,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
variable "migration_admin_users" {
description = "List of users authorized to create a new M4CE sources and perform all other migration operations, in IAM format."
type = list(string)
variable "migration_admin" {
description = "User or group who can create a new M4CE sources and perform all other migration operations, in IAM format (`group:foo@example.com`)."
type = string
}
variable "migration_viewer_users" {
description = "List of users authorized to retrieve information about M4CE in the Google Cloud Console, in IAM format."
type = list(string)
default = []
variable "migration_viewer" {
description = "User or group authorized to retrieve information about M4CE in the Google Cloud Console, in IAM format (`group:foo@example.com`)."
type = string
default = null
}
variable "project_create" {

View File

@ -93,3 +93,19 @@ Once done testing, you can clean up resources by running `terraform destroy`.
| [vm_public_ip_address](outputs.tf#L39) | Azure VM public IP address. | |
<!-- END TFDOC -->
<!--
## Test
```hcl
module "test" {
source = "./fabric/blueprints/cloud-operations/workload-identity-federation"
project_create = {
billing_account_id = "1234-ABCD-1234"
parent = "folders/1234563"
}
project_id = "test-prj"
}
# tftest modules=5 resources=33
```
-->

View File

@ -32,8 +32,11 @@ module "prj" {
"sts.googleapis.com",
]
project_create = var.project_create != null
iam_additive = {
"roles/viewer" : [module.sa.iam_email]
iam_bindings_additive = {
sa_viewer = {
member = module.sa.iam_email
role = "roles/viewer"
}
}
}

View File

@ -85,13 +85,14 @@ This implementation is intentionally minimal and easy to read. A real world use
The example supports the configuration of a Shared VPC as an input variable.
To deploy the solution on a Shared VPC, you have to configure the `network_config` variable:
```
```hcl
network_config = {
host_project = "PROJECT_ID"
network_self_link = "https://www.googleapis.com/compute/v1/projects/PROJECT_ID/global/networks/VPC_NAME"
subnet_self_link = "https://www.googleapis.com/compute/v1/projects/PROJECT_ID/regions/$REGION/subnetworks/SUBNET_NAME"
cloudsql_psa_range = "10.60.0.0/24"
}
host_project = "PROJECT_ID"
network_self_link = "https://www.googleapis.com/compute/v1/projects/PROJECT_ID/global/networks/VPC_NAME"
subnet_self_link = "https://www.googleapis.com/compute/v1/projects/PROJECT_ID/regions/$REGION/subnetworks/SUBNET_NAME"
cloudsql_psa_range = "10.60.0.0/24"
}
# tftest skip
```
To run this example, the Shared VPC project needs to have:
@ -137,7 +138,6 @@ terraform destroy
The above command will delete the associated resources so there will be no billable charges made afterwards.
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
@ -145,13 +145,14 @@ The above command will delete the associated resources so there will be no billa
| [postgres_user_password](variables.tf#L40) | `postgres` user password. | <code>string</code> | ✓ | |
| [prefix](variables.tf#L45) | Prefix used for resource names. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L63) | Project id, references existing project if `project_create` is null. | <code>string</code> | ✓ | |
| [data_eng_principals](variables.tf#L17) | Groups with Service Account Token creator role on service accounts in IAM format, only user supported on CloudSQL, eg 'user@domain.com'. | <code>list&#40;string&#41;</code> | | <code>&#91;&#93;</code> |
| [data_eng_principal](variables.tf#L17) | Group or user in IAM format (`group:foo@example.com`) with permissions to access resources and impersonate service accounts. | <code>string</code> | | <code>null</code> |
| [network_config](variables.tf#L23) | Shared VPC network configurations to use. If null networks will be created in projects with preconfigured values. | <code title="object&#40;&#123;&#10; host_project &#61; string&#10; network_self_link &#61; string&#10; subnet_self_link &#61; string&#10; cloudsql_psa_range &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [postgres_database](variables.tf#L34) | `postgres` database. | <code>string</code> | | <code>&#34;guestbook&#34;</code> |
| [project_create](variables.tf#L54) | Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | <code title="object&#40;&#123;&#10; billing_account_id &#61; string&#10; parent &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [regions](variables.tf#L68) | Map of instance_name => location where instances will be deployed. | <code>map&#40;string&#41;</code> | | <code title="&#123;&#10; primary &#61; &#34;europe-west1&#34;&#10; replica &#61; &#34;europe-west3&#34;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [service_encryption_keys](variables.tf#L81) | Cloud KMS keys to use to encrypt resources. Provide a key for each reagion configured. | <code>map&#40;string&#41;</code> | | <code>null</code> |
| [sql_configuration](variables.tf#L87) | Cloud SQL configuration. | <code title="object&#40;&#123;&#10; availability_type &#61; string&#10; database_version &#61; string&#10; psa_range &#61; string&#10; tier &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; availability_type &#61; &#34;REGIONAL&#34;&#10; database_version &#61; &#34;POSTGRES_13&#34;&#10; psa_range &#61; &#34;10.60.0.0&#47;16&#34;&#10; tier &#61; &#34;db-g1-small&#34;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [sql_users](variables.tf#L103) | Cloud SQL user emails. | <code>list&#40;string&#41;</code> | | <code>&#91;&#93;</code> |
## Outputs
@ -162,16 +163,14 @@ The above command will delete the associated resources so there will be no billa
| [demo_commands](outputs.tf#L27) | Demo commands. | |
| [ips](outputs.tf#L36) | IP address of each instance. | |
| [project_id](outputs.tf#L41) | ID of the project containing all the instances. | |
| [service_accounts](outputs.tf#L46) | Service Accounts. | |
| [service_account](outputs.tf#L46) | SQL client service Accounts. | |
<!-- END TFDOC -->
## Test
```hcl
module "test" {
source = "./fabric/blueprints/data-solutions/cloudsql-multiregion/"
data_eng_principals = ["dataeng@example.com"]
data_eng_principal = "group:dataeng@example.com"
postgres_user_password = "my-root-password"
project_id = "project"
project_create = {
@ -180,5 +179,5 @@ module "test" {
}
prefix = "prefix"
}
# tftest modules=10 resources=52
# tftest modules=9 resources=43
```

View File

@ -39,7 +39,7 @@ module "db" {
}
resource "google_sql_user" "users" {
for_each = toset(var.data_eng_principals)
for_each = toset(var.sql_users)
project = module.project.project_id
name = each.value
instance = module.db.name
@ -47,8 +47,7 @@ resource "google_sql_user" "users" {
}
resource "google_sql_user" "service-account" {
for_each = toset(var.data_eng_principals)
project = module.project.project_id
project = module.project.project_id
# Omit the .gserviceaccount.com suffix in the email
name = regex("(.+)(.gserviceaccount)", module.service-account-sql.email)[0]
instance = module.db.name

View File

@ -1,30 +0,0 @@
# Copyright 2022 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
#
# https://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.
module "gcs" {
source = "../../../modules/gcs"
project_id = module.project.project_id
prefix = var.prefix
name = "data"
location = var.regions.primary
storage_class = "REGIONAL"
encryption_key = var.service_encryption_keys != null ? try(var.service_encryption_keys[var.regions.primary], null) : null
force_destroy = true
}
module "service-account-gcs" {
source = "../../../modules/iam-service-account"
project_id = module.project.project_id
name = "${var.prefix}-gcs"
}

View File

@ -15,54 +15,26 @@
*/
locals {
data_eng_principals_iam = [
for k in var.data_eng_principals :
"user:${k}"
]
iam = {
# GCS roles
"roles/storage.objectAdmin" = [
"serviceAccount:${module.project.service_accounts.robots.sql}",
module.service-account-gcs.iam_email,
iam_roles = {
data_eng = [
"roles/owner"
]
# CloudSQL
"roles/cloudsql.admin" = local.data_eng_principals_iam
"roles/cloudsql.client" = concat(
local.data_eng_principals_iam,
[module.service-account-sql.iam_email]
)
"roles/cloudsql.instanceUser" = concat(
local.data_eng_principals_iam,
[module.service-account-sql.iam_email]
)
# compute engineering
"roles/compute.instanceAdmin.v1" = local.data_eng_principals_iam
"roles/compute.osLogin" = local.data_eng_principals_iam
"roles/compute.viewer" = local.data_eng_principals_iam
"roles/iap.tunnelResourceAccessor" = local.data_eng_principals_iam
# common roles
"roles/logging.admin" = local.data_eng_principals_iam
"roles/iam.serviceAccountUser" = concat(
local.data_eng_principals_iam
)
"roles/iam.serviceAccountTokenCreator" = concat(
local.data_eng_principals_iam
)
# network roles
"roles/compute.networkUser" = [
"serviceAccount:${module.project.service_accounts.robots.sql}"
sql_robot = [
"roles/compute.networkUser",
"roles/storage.objectAdmin"
]
sql_sa = [
"roles/cloudsql.client",
"roles/cloudsql.instanceUser"
]
}
shared_vpc_project = try(var.network_config.host_project, null)
use_shared_vpc = var.network_config != null
subnet = (
local.use_shared_vpc
? var.network_config.subnet_self_link
: values(module.vpc.0.subnet_self_links)[0]
)
use_shared_vpc = var.network_config != null
vpc_self_link = (
local.use_shared_vpc
? var.network_config.network_self_link
@ -77,8 +49,26 @@ module "project" {
billing_account = try(var.project_create.billing_account_id, null)
project_create = var.project_create != null
prefix = var.project_create == null ? null : var.prefix
iam = var.project_create != null ? local.iam : {}
iam_additive = var.project_create == null ? local.iam : {}
iam_bindings_additive = merge(
var.data_eng_principal == null ? {} : {
for r in local.iam_roles.data_eng : "data_eng-${r}" => {
member = var.data_eng_principal
role = r
}
},
{
for r in local.iam_roles.sql_robot : "sql_robot-${r}" => {
member = "serviceAccount:${module.project.service_accounts.robots.sql}"
role = r
}
},
{
for r in local.iam_roles.sql_sa : "sql_sa-${r}" => {
member = module.service-account-sql.iam_email
role = r
}
}
)
services = [
"cloudkms.googleapis.com",
"compute.googleapis.com",
@ -92,12 +82,10 @@ module "project" {
"storage.googleapis.com",
"storage-component.googleapis.com",
]
shared_vpc_service_config = local.shared_vpc_project == null ? null : {
attach = true
host_project = local.shared_vpc_project
}
service_encryption_key_ids = {
compute = try(values(var.service_encryption_keys), [])
sql = try(values(var.service_encryption_keys), [])
@ -120,7 +108,6 @@ module "vpc" {
region = var.regions.primary
}
]
psa_config = {
ranges = { cloud-sql = var.sql_configuration.psa_range }
routes = null
@ -145,3 +132,14 @@ module "nat" {
name = "${var.prefix}-default"
router_network = module.vpc.0.name
}
module "gcs" {
source = "../../../modules/gcs"
project_id = module.project.project_id
prefix = var.prefix
name = "data"
location = var.regions.primary
storage_class = "REGIONAL"
encryption_key = var.service_encryption_keys != null ? try(var.service_encryption_keys[var.regions.primary], null) : null
force_destroy = true
}

View File

@ -43,10 +43,7 @@ output "project_id" {
value = module.project.project_id
}
output "service_accounts" {
description = "Service Accounts."
value = {
"gcs" = module.service-account-gcs.email
"sql" = module.service-account-sql.email
}
output "service_account" {
description = "SQL client service Accounts."
value = module.service-account-sql.email
}

View File

@ -14,10 +14,10 @@
* limitations under the License.
*/
variable "data_eng_principals" {
description = "Groups with Service Account Token creator role on service accounts in IAM format, only user supported on CloudSQL, eg 'user@domain.com'."
type = list(string)
default = []
variable "data_eng_principal" {
description = "Group or user in IAM format (`group:foo@example.com`) with permissions to access resources and impersonate service accounts."
type = string
default = null
}
variable "network_config" {
@ -99,3 +99,9 @@ variable "sql_configuration" {
tier = "db-g1-small"
}
}
variable "sql_users" {
description = "Cloud SQL user emails."
type = list(string)
default = []
}

View File

@ -1,34 +1,39 @@
# Cloud Composer version 2 private instance, supporting Shared VPC and external CMEK key
This blueprint creates a Private instance of [Cloud Composer version 2](https://cloud.google.com/composer/docs/composer-2/composer-versioning-overview) on a VPC with a dedicated service account. Cloud Composer 2 is the new major version for Cloud Composer that supports:
- environment autoscaling
- workloads configuration: CPU, memory, and storage parameters for Airflow workers, schedulers, web server, and database.
- environment autoscaling
- workloads configuration: CPU, memory, and storage parameters for Airflow workers, schedulers, web server, and database.
Please consult the [documentation page](https://cloud.google.com/composer/docs/composer-2/composer-versioning-overview) for an exhaustive comparison between Composer Version 1 and Version 2.
The solution will use:
- Cloud Composer
- VPC with Private Service Access to deploy resources, if no Shared VPC configuration provided.
- Google Cloud NAT to access internet resources, if no Shared VPC configuration provided.
- Cloud Composer
- VPC with Private Service Access to deploy resources, if no Shared VPC configuration provided.
- Google Cloud NAT to access internet resources, if no Shared VPC configuration provided.
The solution supports as inputs:
- Shared VPC
- Cloud KMS CMEK keys
- Shared VPC
- Cloud KMS CMEK keys
This is the high level diagram:
![Cloud Composer 2 architecture overview](./diagram.png "Cloud Composer 2 architecture overview")
# Requirements
## Requirements
This blueprint will deploy all its resources into the project defined by the project_id variable. Please note that we assume this project already exists. However, if you provide the appropriate values to the `project_create` variable, the project will be created as part of the deployment.
If `project_create` is left to null, the identity performing the deployment needs the owner role on the project defined by the `project_id` variable. Otherwise, the identity performing the deployment needs `resourcemanager.projectCreator` on the resource hierarchy node specified by `project_create.parent` and `billing.user` on the billing account specified by `project_create.billing_account_id`.
# Deployment
## Deployment
Run Terraform init:
```bash
$ terraform init
terraform init
```
Configure the Terraform variable in your terraform.tfvars file. You need to specify at least the following variables:
@ -41,23 +46,28 @@ prefix = "lc"
You can run now:
```bash
$ terraform apply
terraform apply
```
You can now connect to your instance.
# Customizations
## Customizations
### VPC
## VPC
If a shared VPC is not configured, a VPC will be created within the project. The following IP ranges will be used:
- Cloudsql: `10.20.10.0/24`
- GKE: `10.20.11.0/28`
Change the code as needed to match your needed configuration, remember that these addresses should not overlap with any other range used in network.
## Shared VPC
As is often the case in real-world configurations, this blueprint accepts as input an existing [`Shared-VPC`](https://cloud.google.com/vpc/docs/shared-vpc) via the `network_config` variable.
### Shared VPC
As is often the case in real-world configurations, this blueprint accepts as input an existing [`Shared-VPC`](https://cloud.google.com/vpc/docs/shared-vpc) via the `network_config` variable.
Example:
```tfvars
network_config = {
host_project = "PROJECT"
@ -68,42 +78,47 @@ network_config = {
services = "services"
}
}
# tftest skip
```
Make sure that:
- The GKE API (`container.googleapis.com`) is enabled in the VPC host project.
- The subnet has secondary ranges configured with 2 ranges:
- pods: `/22` example: `10.10.8.0/22`
- services = `/24` example: 10.10.12.0/24`
- pods: `/22` example: `10.10.8.0/22`
- services = `/24` example: 10.10.12.0/24`
- Firewall rules are set, as described in the [documentation](https://cloud.google.com/composer/docs/composer-2/configure-private-ip#step_3_configure_firewall_rules)
In order to run the example and deploy Cloud Composer on a shared VPC the identity running Terraform must have the following IAM role on the Shared VPC Host project.
- Compute Network Admin (roles/compute.networkAdmin)
- Compute Shared VPC Admin (roles/compute.xpnAdmin)
- Compute Network Admin (roles/compute.networkAdmin)
- Compute Shared VPC Admin (roles/compute.xpnAdmin)
## Encryption
As is often the case in real-world configurations, this blueprint accepts as input an existing [`Cloud KMS keys`](https://cloud.google.com/kms/docs/cmek) via the `service_encryption_keys` variable.
As is often the case in real-world configurations, this blueprint accepts as input an existing [`Cloud KMS keys`](https://cloud.google.com/kms/docs/cmek) via the `service_encryption_keys` variable.
Example:
```tfvars
service_encryption_keys = {
`europe/west1` = `projects/PROJECT/locations/REGION/keyRings/KR_NAME/cryptoKeys/KEY_NAME`
}
# tftest skip
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [prefix](variables.tf#L82) | Prefix used for resource names. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L100) | Project id, references existing project if `project_create` is null. | <code>string</code> | ✓ | |
| [prefix](variables.tf#L83) | Prefix used for resource names. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L101) | Project id, references existing project if `project_create` is null. | <code>string</code> | ✓ | |
| [composer_config](variables.tf#L17) | Composer environment configuration. It accepts only following attributes: `environment_size`, `software_config` and `workloads_config`. See [attribute reference](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/composer_environment#argument-reference---cloud-composer-2) for details on settings variables. | <code title="object&#40;&#123;&#10; environment_size &#61; string&#10; software_config &#61; any&#10; workloads_config &#61; object&#40;&#123;&#10; scheduler &#61; object&#40;&#10; &#123;&#10; cpu &#61; number&#10; memory_gb &#61; number&#10; storage_gb &#61; number&#10; count &#61; number&#10; &#125;&#10; &#41;&#10; web_server &#61; object&#40;&#10; &#123;&#10; cpu &#61; number&#10; memory_gb &#61; number&#10; storage_gb &#61; number&#10; &#125;&#10; &#41;&#10; worker &#61; object&#40;&#10; &#123;&#10; cpu &#61; number&#10; memory_gb &#61; number&#10; storage_gb &#61; number&#10; min_count &#61; number&#10; max_count &#61; number&#10; &#125;&#10; &#41;&#10; &#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; environment_size &#61; &#34;ENVIRONMENT_SIZE_SMALL&#34;&#10; software_config &#61; &#123;&#10; image_version &#61; &#34;composer-2-airflow-2&#34;&#10; &#125;&#10; workloads_config &#61; null&#10;&#125;">&#123;&#8230;&#125;</code> |
| [iam_groups_map](variables.tf#L58) | Map of Role => groups to be added on the project. Example: { \"roles/composer.admin\" = [\"group:gcp-data-engineers@example.com\"]}. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>null</code> |
| [network_config](variables.tf#L64) | Shared VPC network configurations to use. If null networks will be created in projects with preconfigured values. | <code title="object&#40;&#123;&#10; host_project &#61; string&#10; network_self_link &#61; string&#10; subnet_self_link &#61; string&#10; composer_ip_ranges &#61; object&#40;&#123;&#10; cloudsql &#61; string&#10; gke_master &#61; string&#10; &#125;&#41;&#10; composer_secondary_ranges &#61; object&#40;&#123;&#10; pods &#61; string&#10; services &#61; string&#10; &#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [project_create](variables.tf#L91) | Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | <code title="object&#40;&#123;&#10; billing_account_id &#61; string&#10; parent &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [region](variables.tf#L105) | Reagion where instances will be deployed. | <code>string</code> | | <code>&#34;europe-west1&#34;</code> |
| [service_encryption_keys](variables.tf#L111) | Cloud KMS keys to use to encrypt resources. Provide a key for each reagion in use. | <code>map&#40;string&#41;</code> | | <code>null</code> |
| [iam_bindings_additive](variables.tf#L58) | Map of Role => principal in IAM format (`group:foo@example.org`) to be added on the project. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [network_config](variables.tf#L65) | Shared VPC network configurations to use. If null networks will be created in projects with preconfigured values. | <code title="object&#40;&#123;&#10; host_project &#61; string&#10; network_self_link &#61; string&#10; subnet_self_link &#61; string&#10; composer_ip_ranges &#61; object&#40;&#123;&#10; cloudsql &#61; string&#10; gke_master &#61; string&#10; &#125;&#41;&#10; composer_secondary_ranges &#61; object&#40;&#123;&#10; pods &#61; string&#10; services &#61; string&#10; &#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [project_create](variables.tf#L92) | Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | <code title="object&#40;&#123;&#10; billing_account_id &#61; string&#10; parent &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [region](variables.tf#L106) | Reagion where instances will be deployed. | <code>string</code> | | <code>&#34;europe-west1&#34;</code> |
| [service_encryption_keys](variables.tf#L112) | Cloud KMS keys to use to encrypt resources. Provide a key for each reagion in use. | <code>map&#40;string&#41;</code> | | <code>null</code> |
## Outputs
@ -111,7 +126,6 @@ service_encryption_keys = {
|---|---|:---:|
| [composer_airflow_uri](outputs.tf#L17) | The URI of the Apache Airflow Web UI hosted within the Cloud Composer environment.. | |
| [composer_dag_gcs](outputs.tf#L22) | The Cloud Storage prefix of the DAGs for the Cloud Composer environment. | |
<!-- END TFDOC -->
## Test

View File

@ -15,14 +15,8 @@
*/
locals {
iam = merge(
{
"roles/composer.worker" = [module.comp-sa.iam_email]
"roles/composer.ServiceAgentV2Ext" = ["serviceAccount:${module.project.service_accounts.robots.composer}"]
},
var.iam_groups_map
)
# Adding Roles on Service Identities Service account as per documentation: https://cloud.google.com/composer/docs/composer-2/configure-shared-vpc#edit_permissions_for_the_google_apis_service_account
# add Roles on Service Identities service account as per documentation
# https://cloud.google.com/composer/docs/composer-2/configure-shared-vpc#edit_permissions_for_the_google_apis_service_account
_shared_vpc_bindings = {
"roles/compute.networkUser" = [
"prj-cloudservices", "prj-robot-gke"
@ -34,11 +28,16 @@ locals {
"prj-robot-gke"
]
}
shared_vpc_role_members = {
prj-cloudservices = "serviceAccount:${module.project.service_accounts.cloud_services}"
prj-robot-gke = "serviceAccount:${module.project.service_accounts.robots.container-engine}"
prj-robot-cs = "serviceAccount:${module.project.service_accounts.robots.composer}"
}
orch_subnet = (
local.use_shared_vpc
? var.network_config.subnet_self_link
: values(module.vpc.0.subnet_self_links)[0]
)
orch_vpc = (
local.use_shared_vpc
? var.network_config.network_self_link
: module.vpc.0.self_link
)
# reassemble in a format suitable for for_each
shared_vpc_bindings_map = {
for binding in flatten([
@ -47,27 +46,24 @@ locals {
]
]) : "${binding.role}-${binding.member}" => binding
}
shared_vpc_project = try(var.network_config.host_project, null)
use_shared_vpc = var.network_config != null
shared_vpc_role_members = {
prj-cloudservices = (
"serviceAccount:${module.project.service_accounts.cloud_services}"
)
prj-robot-gke = (
"serviceAccount:${module.project.service_accounts.robots.container-engine}"
)
prj-robot-cs = (
"serviceAccount:${module.project.service_accounts.robots.composer}"
)
}
use_shared_vpc = var.network_config != null
vpc_self_link = (
local.use_shared_vpc
? var.network_config.network_self_link
: module.vpc.0.self_link
)
orch_subnet = (
local.use_shared_vpc
? var.network_config.subnet_self_link
: values(module.vpc.0.subnet_self_links)[0]
)
orch_vpc = (
local.use_shared_vpc
? var.network_config.network_self_link
: module.vpc.0.self_link
)
}
module "project" {
@ -77,8 +73,24 @@ module "project" {
billing_account = try(var.project_create.billing_account_id, null)
project_create = var.project_create != null
prefix = var.project_create == null ? null : var.prefix
iam = var.project_create != null ? local.iam : {}
iam_additive = var.project_create == null ? local.iam : {}
iam_bindings_additive = merge(
{
composer_worker = {
member = module.comp-sa.iam_email
role = "roles/composer.worker"
},
composer_service_agent = {
member = "serviceAccount:${module.project.service_accounts.robots.composer}"
role = "roles/composer.ServiceAgentV2Ext"
}
},
{
for k, v in var.iam_bindings_additive : "${k}:${v}" => {
member = v
role = k
}
}
)
services = [
"artifactregistry.googleapis.com",
"cloudkms.googleapis.com",
@ -94,19 +106,13 @@ module "project" {
"storage.googleapis.com",
"storage-component.googleapis.com",
]
shared_vpc_service_config = local.shared_vpc_project == null ? null : {
attach = true
host_project = local.shared_vpc_project
}
service_encryption_key_ids = {
composer = [try(lookup(var.service_encryption_keys, var.region, null), null)]
}
service_config = {
disable_on_destroy = false, disable_dependent_services = false
}
}
module "vpc" {

View File

@ -55,10 +55,11 @@ variable "composer_config" {
}
}
variable "iam_groups_map" {
description = "Map of Role => groups to be added on the project. Example: { \"roles/composer.admin\" = [\"group:gcp-data-engineers@example.com\"]}."
variable "iam_bindings_additive" {
description = "Map of Role => principal in IAM format (`group:foo@example.org`) to be added on the project."
type = map(list(string))
default = null
nullable = false
default = {}
}
variable "network_config" {

View File

@ -15,21 +15,28 @@
# tfdoc:file:description drop off project and resources.
locals {
iam_drp = {
"roles/bigquery.dataEditor" = [
module.drop-sa-bq-0.iam_email, local.groups_iam.data-engineers
drp_iam = {
data_engineers = [
"roles/bigquery.dataEditor",
"roles/bigquery.user"
]
"roles/bigquery.user" = [
module.load-sa-df-0.iam_email, local.groups_iam.data-engineers
sa_drop_bq = [
"roles/bigquery.dataEditor"
]
"roles/pubsub.publisher" = [module.drop-sa-ps-0.iam_email]
"roles/pubsub.subscriber" = [
module.orch-sa-cmp-0.iam_email, module.load-sa-df-0.iam_email
sa_drop_cs = [
"roles/storage.objectCreator"
]
"roles/storage.objectCreator" = [module.drop-sa-cs-0.iam_email]
"roles/storage.objectViewer" = [module.orch-sa-cmp-0.iam_email]
"roles/storage.objectAdmin" = [
module.load-sa-df-0.iam_email, module.load-sa-df-0.iam_email
sa_drop_ps = [
"roles/pubsub.publisher"
]
sa_load = [
"roles/bigquery.user",
"roles/pubsub.subscriber",
"roles/storage.objectAdmin"
]
sa_orch = [
"roles/pubsub.subscriber",
"roles/storage.objectViewer"
]
}
}
@ -39,10 +46,14 @@ module "drop-project" {
parent = var.project_config.parent
billing_account = var.project_config.billing_account_id
project_create = var.project_config.billing_account_id != null
prefix = var.project_config.billing_account_id == null ? null : var.prefix
name = var.project_config.billing_account_id == null ? var.project_config.project_ids.drop : "${var.project_config.project_ids.drop}${local.project_suffix}"
iam = var.project_config.billing_account_id != null ? local.iam_drp : null
iam_additive = var.project_config.billing_account_id == null ? local.iam_drp : null
prefix = local.use_projects ? null : var.prefix
name = (
local.use_projects
? var.project_config.project_ids.drop
: "${var.project_config.project_ids.drop}${local.project_suffix}"
)
iam = local.use_projects ? {} : local.drp_iam_auth
iam_bindings_additive = !local.use_projects ? {} : local.drp_iam_additive
services = concat(var.project_services, [
"bigquery.googleapis.com",
"bigqueryreservation.googleapis.com",
@ -59,8 +70,6 @@ module "drop-project" {
}
}
# Cloud Storage
module "drop-sa-cs-0" {
source = "../../../modules/iam-service-account"
project_id = module.drop-project.project_id
@ -89,8 +98,6 @@ module "drop-cs-0" {
# }
}
# PubSub
module "drop-sa-ps-0" {
source = "../../../modules/iam-service-account"
project_id = module.drop-project.project_id
@ -111,8 +118,6 @@ module "drop-ps-0" {
kms_key = try(local.service_encryption_keys.pubsub, null)
}
# BigQuery
module "drop-sa-bq-0" {
source = "../../../modules/iam-service-account"
project_id = module.drop-project.project_id

View File

@ -15,46 +15,38 @@
# tfdoc:file:description Load project and VPC.
locals {
iam_load = {
"roles/bigquery.jobUser" = [module.load-sa-df-0.iam_email]
"roles/dataflow.admin" = [
module.orch-sa-cmp-0.iam_email,
module.load-sa-df-0.iam_email,
local.groups_iam.data-engineers
load_iam = {
data_engineers = [
"roles/dataflow.admin"
]
"roles/dataflow.developer" = [
local.groups_iam.data-engineers
robots_dataflow_load = [
"roles/storage.objectAdmin"
]
sa_load = [
"roles/bigquery.jobUser",
"roles/dataflow.admin",
"roles/dataflow.worker",
"roles/storage.objectAdmin"
]
sa_orch = [
"roles/dataflow.admin"
]
"roles/dataflow.worker" = [module.load-sa-df-0.iam_email]
"roles/storage.objectAdmin" = local.load_service_accounts
}
load_service_accounts = [
"serviceAccount:${module.load-project.service_accounts.robots.dataflow}",
module.load-sa-df-0.iam_email
]
load_subnet = (
local.use_shared_vpc
? var.network_config.subnet_self_links.orchestration
: values(module.load-vpc.0.subnet_self_links)[0]
)
load_vpc = (
local.use_shared_vpc
? var.network_config.network_self_link
: module.load-vpc.0.self_link
)
}
# Project
module "load-project" {
source = "../../../modules/project"
parent = var.project_config.parent
billing_account = var.project_config.billing_account_id
project_create = var.project_config.billing_account_id != null
prefix = var.project_config.billing_account_id == null ? null : var.prefix
name = var.project_config.billing_account_id == null ? var.project_config.project_ids.load : "${var.project_config.project_ids.load}${local.project_suffix}"
iam = var.project_config.billing_account_id != null ? local.iam_load : null
iam_additive = var.project_config.billing_account_id == null ? local.iam_load : null
prefix = local.use_projects ? null : var.prefix
name = (
local.use_projects
? var.project_config.project_ids.load
: "${var.project_config.project_ids.load}${local.project_suffix}"
)
iam = local.use_projects ? {} : local.load_iam_auth
iam_bindings_additive = !local.use_projects ? {} : local.load_iam_additive
services = concat(var.project_services, [
"bigquery.googleapis.com",
"bigqueryreservation.googleapis.com",
@ -106,8 +98,6 @@ module "load-cs-df-0" {
encryption_key = try(local.service_encryption_keys.storage, null)
}
# internal VPC resources
module "load-vpc" {
source = "../../../modules/net-vpc"
count = local.use_shared_vpc ? 0 : 1

View File

@ -15,58 +15,39 @@
# tfdoc:file:description Orchestration project and VPC.
locals {
iam_orch = {
"roles/artifactregistry.admin" = [local.groups_iam.data-engineers]
"roles/artifactregistry.reader" = [module.load-sa-df-0.iam_email]
"roles/bigquery.dataEditor" = [
module.load-sa-df-0.iam_email,
module.transf-sa-df-0.iam_email,
local.groups_iam.data-engineers
orch_iam = {
data_engineers = [
"roles/artifactregistry.admin",
"roles/bigquery.dataEditor",
"roles/bigquery.jobUser",
"roles/cloudbuild.builds.editor",
"roles/composer.environmentAndStorageObjectAdmin",
"roles/iam.serviceAccountUser",
"roles/iap.httpsResourceAccessor",
"roles/serviceusage.serviceUsageConsumer"
]
"roles/bigquery.jobUser" = [
module.orch-sa-cmp-0.iam_email,
local.groups_iam.data-engineers
robots_cloudbuild = [
"roles/storage.objectAdmin"
]
"roles/cloudbuild.builds.editor" = [local.groups_iam.data-engineers]
"roles/cloudbuild.serviceAgent" = [module.orch-sa-df-build.iam_email]
"roles/composer.admin" = [local.groups_iam.data-engineers]
"roles/composer.user" = [local.groups_iam.data-engineers]
"roles/composer.environmentAndStorageObjectAdmin" = [local.groups_iam.data-engineers]
"roles/composer.ServiceAgentV2Ext" = [
"serviceAccount:${module.orch-project.service_accounts.robots.composer}"
robots_composer = [
"roles/composer.ServiceAgentV2Ext",
"roles/storage.objectAdmin"
]
"roles/composer.worker" = [
module.orch-sa-cmp-0.iam_email
sa_load = [
"roles/artifactregistry.reader",
"roles/bigquery.dataEditor",
"roles/storage.objectViewer"
]
"roles/iam.serviceAccountUser" = [
module.orch-sa-cmp-0.iam_email, local.groups_iam.data-engineers
sa_orch = [
"roles/bigquery.jobUser",
"roles/composer.worker",
"roles/iam.serviceAccountUser",
"roles/storage.objectAdmin"
]
"roles/iap.httpsResourceAccessor" = [local.groups_iam.data-engineers]
"roles/serviceusage.serviceUsageConsumer" = [local.groups_iam.data-engineers]
"roles/storage.objectAdmin" = [
module.orch-sa-cmp-0.iam_email,
module.orch-sa-df-build.iam_email,
"serviceAccount:${module.orch-project.service_accounts.robots.composer}",
"serviceAccount:${module.orch-project.service_accounts.robots.cloudbuild}",
local.groups_iam.data-engineers
sa_transf_df = [
"roles/bigquery.dataEditor"
]
"roles/storage.objectViewer" = [module.load-sa-df-0.iam_email]
}
orch_subnet = (
local.use_shared_vpc
? var.network_config.subnet_self_links.orchestration
: values(module.orch-vpc.0.subnet_self_links)[0]
)
orch_vpc = (
local.use_shared_vpc
? var.network_config.network_self_link
: module.orch-vpc.0.self_link
)
# Note: This formatting is needed for output purposes since the fabric artifact registry
# module doesn't yet expose the docker usage path of a registry folder in the needed format.
orch_docker_path = format("%s-docker.pkg.dev/%s/%s",
var.region, module.orch-project.project_id, module.orch-artifact-reg.name)
}
module "orch-project" {
@ -74,15 +55,17 @@ module "orch-project" {
parent = var.project_config.parent
billing_account = var.project_config.billing_account_id
project_create = var.project_config.billing_account_id != null
prefix = var.project_config.billing_account_id == null ? null : var.prefix
prefix = local.use_projects ? null : var.prefix
name = (
var.project_config.billing_account_id == null
local.use_projects
? var.project_config.project_ids.orc
: "${var.project_config.project_ids.orc}${local.project_suffix}"
)
iam = var.project_config.billing_account_id != null ? local.iam_orch : null
iam_additive = var.project_config.billing_account_id == null ? local.iam_orch : null
oslogin = false
iam = local.use_projects ? {} : local.orch_iam_auth
iam_bindings_additive = !local.use_projects ? {} : local.orch_iam_additive
compute_metadata = {
enable-oslogin = "false"
}
services = concat(var.project_services, [
"artifactregistry.googleapis.com",
"bigquery.googleapis.com",
@ -112,8 +95,6 @@ module "orch-project" {
}
}
# Cloud Storage
module "orch-cs-0" {
source = "../../../modules/gcs"
project_id = module.orch-project.project_id
@ -124,8 +105,6 @@ module "orch-cs-0" {
encryption_key = try(local.service_encryption_keys.storage, null)
}
# internal VPC resources
module "orch-vpc" {
source = "../../../modules/net-vpc"
count = local.use_shared_vpc ? 0 : 1

View File

@ -15,29 +15,25 @@
# tfdoc:file:description Transformation project and VPC.
locals {
iam_trf = {
"roles/bigquery.jobUser" = [
module.transf-sa-bq-0.iam_email, local.groups_iam.data-engineers
trf_iam = {
data_engineers = [
"roles/bigquery.jobUser",
"roles/dataflow.admin"
]
"roles/dataflow.admin" = [
module.orch-sa-cmp-0.iam_email, local.groups_iam.data-engineers
robots_dataflow_trf = [
"roles/storage.objectAdmin"
]
"roles/dataflow.worker" = [module.transf-sa-df-0.iam_email]
"roles/storage.objectAdmin" = [
module.transf-sa-df-0.iam_email,
"serviceAccount:${module.transf-project.service_accounts.robots.dataflow}"
sa_orch = [
"roles/dataflow.admin"
]
sa_transf_bq = [
"roles/bigquery.jobUser"
]
sa_transf_df = [
"roles/dataflow.worker",
"roles/storage.objectAdmin"
]
}
transf_subnet = (
local.use_shared_vpc
? var.network_config.subnet_self_links.orchestration
: values(module.transf-vpc.0.subnet_self_links)[0]
)
transf_vpc = (
local.use_shared_vpc
? var.network_config.network_self_link
: module.transf-vpc.0.self_link
)
}
module "transf-project" {
@ -45,10 +41,14 @@ module "transf-project" {
parent = var.project_config.parent
billing_account = var.project_config.billing_account_id
project_create = var.project_config.billing_account_id != null
prefix = var.project_config.billing_account_id == null ? null : var.prefix
name = var.project_config.billing_account_id == null ? var.project_config.project_ids.trf : "${var.project_config.project_ids.trf}${local.project_suffix}"
iam = var.project_config.billing_account_id != null ? local.iam_trf : null
iam_additive = var.project_config.billing_account_id == null ? local.iam_trf : null
prefix = local.use_projects ? null : var.prefix
name = (
local.use_projects
? var.project_config.project_ids.trf
: "${var.project_config.project_ids.trf}${local.project_suffix}"
)
iam = local.use_projects ? {} : local.trf_iam_auth
iam_bindings_additive = !local.use_projects ? {} : local.trf_iam_additive
services = concat(var.project_services, [
"bigquery.googleapis.com",
"bigqueryreservation.googleapis.com",
@ -72,8 +72,6 @@ module "transf-project" {
}
}
# Cloud Storage
module "transf-sa-df-0" {
source = "../../../modules/iam-service-account"
project_id = module.transf-project.project_id
@ -101,8 +99,6 @@ module "transf-cs-df-0" {
encryption_key = try(local.service_encryption_keys.storage, null)
}
# BigQuery
module "transf-sa-bq-0" {
source = "../../../modules/iam-service-account"
project_id = module.transf-project.project_id
@ -120,8 +116,6 @@ module "transf-sa-bq-0" {
}
}
# internal VPC resources
module "transf-vpc" {
source = "../../../modules/net-vpc"
count = local.use_shared_vpc ? 0 : 1

View File

@ -15,61 +15,48 @@
# tfdoc:file:description Data Warehouse projects.
locals {
dwh_lnd_iam = {
"roles/bigquery.dataOwner" = [
module.load-sa-df-0.iam_email,
]
"roles/bigquery.dataViewer" = [
module.transf-sa-df-0.iam_email,
module.transf-sa-bq-0.iam_email,
local.groups_iam.data-engineers
]
"roles/bigquery.jobUser" = [
module.load-sa-df-0.iam_email, local.groups_iam.data-engineers
]
"roles/datacatalog.categoryAdmin" = [module.transf-sa-bq-0.iam_email]
"roles/datacatalog.tagTemplateViewer" = [local.groups_iam.data-engineers]
"roles/datacatalog.viewer" = [local.groups_iam.data-engineers]
"roles/storage.objectCreator" = [module.load-sa-df-0.iam_email]
"roles/storage.objectViewer" = [local.groups_iam.data-engineers]
}
dwh_iam = {
"roles/bigquery.dataOwner" = [
module.transf-sa-df-0.iam_email,
module.transf-sa-bq-0.iam_email,
data_analysts = [
"roles/bigquery.dataViewer",
"roles/bigquery.jobUser",
"roles/datacatalog.viewer",
"roles/storage.objectViewer"
]
"roles/bigquery.dataViewer" = [
local.groups_iam.data-analysts,
local.groups_iam.data-engineers
data_engineers = [
"roles/bigquery.dataViewer",
"roles/bigquery.jobUser",
"roles/datacatalog.viewer",
"roles/storage.objectViewer"
]
"roles/bigquery.jobUser" = [
module.transf-sa-bq-0.iam_email,
local.groups_iam.data-analysts,
local.groups_iam.data-engineers
sa_transf_bq = [
"roles/bigquery.dataOwner",
"roles/bigquery.jobUser"
]
"roles/datacatalog.tagTemplateViewer" = [
local.groups_iam.data-analysts, local.groups_iam.data-engineers
sa_transf_df = [
"roles/bigquery.dataOwner",
"roles/storage.objectAdmin"
]
}
lnd_iam = {
data_engineers = [
"roles/bigquery.dataViewer",
"roles/bigquery.jobUser",
"roles/datacatalog.viewer",
"roles/storage.objectViewer"
]
sa_load = [
"roles/storage.objectCreator"
]
sa_transf_bq = [
"roles/bigquery.dataViewer",
"roles/datacatalog.categoryAdmin"
]
sa_transf_df = [
"roles/bigquery.dataOwner",
"roles/bigquery.dataViewer",
"roles/bigquery.jobUser"
]
"roles/datacatalog.viewer" = [
local.groups_iam.data-analysts, local.groups_iam.data-engineers
]
"roles/storage.objectViewer" = [
local.groups_iam.data-analysts, local.groups_iam.data-engineers
]
"roles/storage.objectAdmin" = [module.transf-sa-df-0.iam_email]
}
dwh_services = concat(var.project_services, [
"bigquery.googleapis.com",
"bigqueryreservation.googleapis.com",
"bigquerystorage.googleapis.com",
"cloudkms.googleapis.com",
"compute.googleapis.com",
"dataflow.googleapis.com",
"pubsub.googleapis.com",
"servicenetworking.googleapis.com",
"storage.googleapis.com",
"storage-component.googleapis.com"
])
}
# Project
@ -79,11 +66,15 @@ module "dwh-lnd-project" {
parent = var.project_config.parent
billing_account = var.project_config.billing_account_id
project_create = var.project_config.billing_account_id != null
prefix = var.project_config.billing_account_id == null ? null : var.prefix
name = var.project_config.billing_account_id == null ? var.project_config.project_ids.dwh-lnd : "${var.project_config.project_ids.dwh-lnd}${local.project_suffix}"
iam = var.project_config.billing_account_id != null ? local.dwh_lnd_iam : {}
iam_additive = var.project_config.billing_account_id == null ? local.dwh_lnd_iam : {}
services = local.dwh_services
prefix = local.use_projects ? null : var.prefix
name = (
local.use_projects
? var.project_config.project_ids.dwh-lnd
: "${var.project_config.project_ids.dwh-lnd}${local.project_suffix}"
)
iam = local.use_projects ? {} : local.lnd_iam_auth
iam_bindings_additive = !local.use_projects ? {} : local.lnd_iam_additive
services = local.dwh_services
service_encryption_key_ids = {
bq = [try(local.service_encryption_keys.bq, null)]
storage = [try(local.service_encryption_keys.storage, null)]
@ -95,11 +86,15 @@ module "dwh-cur-project" {
parent = var.project_config.parent
billing_account = var.project_config.billing_account_id
project_create = var.project_config.billing_account_id != null
prefix = var.project_config.billing_account_id == null ? null : var.prefix
name = var.project_config.billing_account_id == null ? var.project_config.project_ids.dwh-cur : "${var.project_config.project_ids.dwh-cur}${local.project_suffix}"
iam = var.project_config.billing_account_id != null ? local.dwh_iam : {}
iam_additive = var.project_config.billing_account_id == null ? local.dwh_iam : {}
services = local.dwh_services
prefix = local.use_projects ? null : var.prefix
name = (
local.use_projects
? var.project_config.project_ids.dwh-cur
: "${var.project_config.project_ids.dwh-cur}${local.project_suffix}"
)
iam = local.use_projects ? {} : local.dwh_iam_auth
iam_bindings_additive = !local.use_projects ? {} : local.dwh_iam_additive
services = local.dwh_services
service_encryption_key_ids = {
bq = [try(local.service_encryption_keys.bq, null)]
storage = [try(local.service_encryption_keys.storage, null)]
@ -111,19 +106,21 @@ module "dwh-conf-project" {
parent = var.project_config.parent
billing_account = var.project_config.billing_account_id
project_create = var.project_config.billing_account_id != null
prefix = var.project_config.billing_account_id == null ? null : var.prefix
name = var.project_config.billing_account_id == null ? var.project_config.project_ids.dwh-conf : "${var.project_config.project_ids.dwh-conf}${local.project_suffix}"
iam = var.project_config.billing_account_id != null ? local.dwh_iam : null
iam_additive = var.project_config.billing_account_id == null ? local.dwh_iam : null
services = local.dwh_services
prefix = local.use_projects ? null : var.prefix
name = (
local.use_projects
? var.project_config.project_ids.dwh-conf
: "${var.project_config.project_ids.dwh-conf}${local.project_suffix}"
)
iam = local.use_projects ? {} : local.dwh_iam_auth
iam_bindings_additive = !local.use_projects ? {} : local.dwh_iam_additive
services = local.dwh_services
service_encryption_key_ids = {
bq = [try(local.service_encryption_keys.bq, null)]
storage = [try(local.service_encryption_keys.storage, null)]
}
}
# Bigquery
module "dwh-lnd-bq-0" {
source = "../../../modules/bigquery-dataset"
project_id = module.dwh-lnd-project.project_id
@ -148,8 +145,6 @@ module "dwh-conf-bq-0" {
encryption_key = try(local.service_encryption_keys.bq, null)
}
# Cloud storage
module "dwh-lnd-cs-0" {
source = "../../../modules/gcs"
project_id = module.dwh-lnd-project.project_id

View File

@ -15,47 +15,56 @@
# tfdoc:file:description common project.
locals {
iam_common = {
"roles/dlp.admin" = [local.groups_iam.data-security]
"roles/dlp.estimatesAdmin" = [local.groups_iam.data-engineers]
"roles/dlp.reader" = [local.groups_iam.data-engineers]
"roles/dlp.user" = [
module.load-sa-df-0.iam_email,
module.transf-sa-df-0.iam_email,
local.groups_iam.data-engineers
cmn_iam = {
data_analysts = [
# uncomment if access to all tagged columns is needed
# "roles/datacatalog.categoryFineGrainedReader",
"roles/datacatalog.viewer"
]
"roles/datacatalog.admin" = [local.groups_iam.data-security]
"roles/datacatalog.viewer" = [
module.load-sa-df-0.iam_email,
module.transf-sa-df-0.iam_email,
module.transf-sa-bq-0.iam_email,
local.groups_iam.data-analysts
data_engineers = [
"roles/dlp.estimatesAdmin",
"roles/dlp.reader",
"roles/dlp.user"
]
"roles/datacatalog.categoryFineGrainedReader" = [
module.transf-sa-df-0.iam_email,
module.transf-sa-bq-0.iam_email,
# Uncomment if you want to grant access to `data-analyst` to all columns tagged.
# local.groups_iam.data-analysts
data_security = [
"roles/datacatalog.admin",
"roles/dlp.admin"
]
sa_load = [
"roles/datacatalog.viewer",
"roles/dlp.user"
]
sa_transf_bq = [
"roles/datacatalog.categoryFineGrainedReader",
"roles/datacatalog.viewer"
]
sa_transf_df = [
"roles/datacatalog.categoryFineGrainedReader",
"roles/datacatalog.viewer",
"roles/dlp.user"
]
}
}
module "common-project" {
source = "../../../modules/project"
parent = var.project_config.parent
billing_account = var.project_config.billing_account_id
project_create = var.project_config.billing_account_id != null
prefix = var.project_config.billing_account_id == null ? null : var.prefix
name = var.project_config.billing_account_id == null ? var.project_config.project_ids.common : "${var.project_config.project_ids.common}${local.project_suffix}"
iam = var.project_config.billing_account_id != null ? local.iam_common : null
iam_additive = var.project_config.billing_account_id == null ? local.iam_common : null
prefix = local.use_projects ? null : var.prefix
name = (
local.use_projects
? var.project_config.project_ids.common
: "${var.project_config.project_ids.common}${local.project_suffix}"
)
iam = local.use_projects ? {} : local.cmn_iam_auth
iam_bindings_additive = !local.use_projects ? {} : local.cmn_iam_additive
services = concat(var.project_services, [
"datacatalog.googleapis.com",
"dlp.googleapis.com",
])
}
# Data Catalog Policy tag
module "common-datacatalog" {
source = "../../../modules/data-catalog-policy-tag"
project_id = module.common-project.project_id
@ -64,7 +73,8 @@ module "common-datacatalog" {
tags = var.data_catalog_tags
}
# To create KMS keys in the common project: uncomment this section and assigne key links accondingly in local.service_encryption_keys variable
# To create KMS keys in the common project: uncomment this section
# and assign key links accondingly in local.service_encryption_keys variable
# module "cmn-kms-0" {
# source = "../../../modules/kms"

View File

@ -19,6 +19,10 @@ module "exp-project" {
parent = var.project_config.parent
billing_account = var.project_config.billing_account_id
project_create = var.project_config.billing_account_id != null
prefix = var.project_config.billing_account_id == null ? null : var.prefix
name = var.project_config.billing_account_id == null ? var.project_config.project_ids.exp : "${var.project_config.project_ids.exp}${local.project_suffix}"
prefix = local.use_projects ? null : var.prefix
name = (
local.use_projects
? var.project_config.project_ids.exp
: "${var.project_config.project_ids.exp}${local.project_suffix}"
)
}

View File

@ -202,8 +202,7 @@ project_config = {
parent = "folders/1111111111"
billing_account_id = "1111111-2222222-33333333"
}
organization_domain = "domain.com"
~
organization_domain = "domain.com"
```
For more fine details check variables on [`variables.tf`](./variables.tf) and update according to the desired configuration. Remember to create team groups described [below](#groups).
@ -229,8 +228,7 @@ module "data-platform" {
}
prefix = "myprefix"
}
# tftest modules=43 resources=285
# tftest modules=43 resources=279
```
## Customizations
@ -262,19 +260,19 @@ You can find examples in the `[demo](./demo)` folder.
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [organization_domain](variables.tf#L159) | Organization domain. | <code>string</code> | ✓ | |
| [prefix](variables.tf#L164) | Prefix used for resource names. | <code>string</code> | ✓ | |
| [project_config](variables.tf#L173) | Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | <code title="object&#40;&#123;&#10; billing_account_id &#61; optional&#40;string, null&#41;&#10; parent &#61; string&#10; project_ids &#61; optional&#40;object&#40;&#123;&#10; drop &#61; string&#10; load &#61; string&#10; orc &#61; string&#10; trf &#61; string&#10; dwh-lnd &#61; string&#10; dwh-cur &#61; string&#10; dwh-conf &#61; string&#10; common &#61; string&#10; exp &#61; string&#10; &#125;&#41;, &#123;&#10; drop &#61; &#34;drp&#34;&#10; load &#61; &#34;lod&#34;&#10; orc &#61; &#34;orc&#34;&#10; trf &#61; &#34;trf&#34;&#10; dwh-lnd &#61; &#34;dwh-lnd&#34;&#10; dwh-cur &#61; &#34;dwh-cur&#34;&#10; dwh-conf &#61; &#34;dwh-conf&#34;&#10; common &#61; &#34;cmn&#34;&#10; exp &#61; &#34;exp&#34;&#10; &#125;&#10; &#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [composer_config](variables.tf#L17) | Cloud Composer config. | <code title="object&#40;&#123;&#10; disable_deployment &#61; optional&#40;bool&#41;&#10; environment_size &#61; optional&#40;string, &#34;ENVIRONMENT_SIZE_SMALL&#34;&#41;&#10; software_config &#61; optional&#40;object&#40;&#123;&#10; airflow_config_overrides &#61; optional&#40;any&#41;&#10; pypi_packages &#61; optional&#40;any&#41;&#10; env_variables &#61; optional&#40;map&#40;string&#41;&#41;&#10; image_version &#61; string&#10; &#125;&#41;, &#123;&#10; image_version &#61; &#34;composer-2-airflow-2&#34;&#10; &#125;&#41;&#10; workloads_config &#61; optional&#40;object&#40;&#123;&#10; scheduler &#61; optional&#40;object&#40;&#10; &#123;&#10; cpu &#61; number&#10; memory_gb &#61; number&#10; storage_gb &#61; number&#10; count &#61; number&#10; &#125;&#10; &#41;, &#123;&#10; cpu &#61; 0.5&#10; memory_gb &#61; 1.875&#10; storage_gb &#61; 1&#10; count &#61; 1&#10; &#125;&#41;&#10; web_server &#61; optional&#40;object&#40;&#10; &#123;&#10; cpu &#61; number&#10; memory_gb &#61; number&#10; storage_gb &#61; number&#10; &#125;&#10; &#41;, &#123;&#10; cpu &#61; 0.5&#10; memory_gb &#61; 1.875&#10; storage_gb &#61; 1&#10; &#125;&#41;&#10; worker &#61; optional&#40;object&#40;&#10; &#123;&#10; cpu &#61; number&#10; memory_gb &#61; number&#10; storage_gb &#61; number&#10; min_count &#61; number&#10; max_count &#61; number&#10; &#125;&#10; &#41;, &#123;&#10; cpu &#61; 0.5&#10; memory_gb &#61; 1.875&#10; storage_gb &#61; 1&#10; min_count &#61; 1&#10; max_count &#61; 3&#10; &#125;&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; environment_size &#61; &#34;ENVIRONMENT_SIZE_SMALL&#34;&#10; software_config &#61; &#123;&#10; image_version &#61; &#34;composer-2-airflow-2&#34;&#10; &#125;&#10; workloads_config &#61; &#123;&#10; scheduler &#61; &#123;&#10; cpu &#61; 0.5&#10; memory_gb &#61; 1.875&#10; storage_gb &#61; 1&#10; count &#61; 1&#10; &#125;&#10; web_server &#61; &#123;&#10; cpu &#61; 0.5&#10; memory_gb &#61; 1.875&#10; storage_gb &#61; 1&#10; &#125;&#10; worker &#61; &#123;&#10; cpu &#61; 0.5&#10; memory_gb &#61; 1.875&#10; storage_gb &#61; 1&#10; min_count &#61; 1&#10; max_count &#61; 3&#10; &#125;&#10; &#125;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [data_catalog_tags](variables.tf#L100) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | <code title="map&#40;object&#40;&#123;&#10; description &#61; optional&#40;string&#41;&#10; iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code title="&#123;&#10; &#34;3_Confidential&#34; &#61; &#123;&#125;&#10; &#34;2_Private&#34; &#61; &#123;&#125;&#10; &#34;1_Sensitive&#34; &#61; &#123;&#125;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [data_force_destroy](variables.tf#L114) | Flag to set 'force_destroy' on data services like BiguQery or Cloud Storage. | <code>bool</code> | | <code>false</code> |
| [groups](variables.tf#L120) | User groups. | <code>map&#40;string&#41;</code> | | <code title="&#123;&#10; data-analysts &#61; &#34;gcp-data-analysts&#34;&#10; data-engineers &#61; &#34;gcp-data-engineers&#34;&#10; data-security &#61; &#34;gcp-data-security&#34;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [location](variables.tf#L130) | Location used for multi-regional resources. | <code>string</code> | | <code>&#34;eu&#34;</code> |
| [network_config](variables.tf#L136) | Shared VPC network configurations to use. If null networks will be created in projects with preconfigured values. | <code title="object&#40;&#123;&#10; host_project &#61; string&#10; network_self_link &#61; string&#10; subnet_self_links &#61; object&#40;&#123;&#10; load &#61; string&#10; transformation &#61; string&#10; orchestration &#61; string&#10; &#125;&#41;&#10; composer_ip_ranges &#61; object&#40;&#123;&#10; cloudsql &#61; string&#10; gke_master &#61; string&#10; &#125;&#41;&#10; composer_secondary_ranges &#61; object&#40;&#123;&#10; pods &#61; string&#10; services &#61; string&#10; &#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [project_services](variables.tf#L207) | List of core services enabled on all projects. | <code>list&#40;string&#41;</code> | | <code title="&#91;&#10; &#34;cloudresourcemanager.googleapis.com&#34;,&#10; &#34;iam.googleapis.com&#34;,&#10; &#34;serviceusage.googleapis.com&#34;,&#10; &#34;stackdriver.googleapis.com&#34;&#10;&#93;">&#91;&#8230;&#93;</code> |
| [project_suffix](variables.tf#L218) | Suffix used only for project ids. | <code>string</code> | | <code>null</code> |
| [region](variables.tf#L224) | Region used for regional resources. | <code>string</code> | | <code>&#34;europe-west1&#34;</code> |
| [service_encryption_keys](variables.tf#L230) | Cloud KMS to use to encrypt different services. Key location should match service region. | <code title="object&#40;&#123;&#10; bq &#61; string&#10; composer &#61; string&#10; dataflow &#61; string&#10; storage &#61; string&#10; pubsub &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [organization_domain](variables.tf#L164) | Organization domain. | <code>string</code> | ✓ | |
| [prefix](variables.tf#L169) | Prefix used for resource names. | <code>string</code> | ✓ | |
| [project_config](variables.tf#L178) | Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | <code title="object&#40;&#123;&#10; billing_account_id &#61; optional&#40;string, null&#41;&#10; parent &#61; string&#10; project_ids &#61; optional&#40;object&#40;&#123;&#10; drop &#61; string&#10; load &#61; string&#10; orc &#61; string&#10; trf &#61; string&#10; dwh-lnd &#61; string&#10; dwh-cur &#61; string&#10; dwh-conf &#61; string&#10; common &#61; string&#10; exp &#61; string&#10; &#125;&#41;, &#123;&#10; drop &#61; &#34;drp&#34;&#10; load &#61; &#34;lod&#34;&#10; orc &#61; &#34;orc&#34;&#10; trf &#61; &#34;trf&#34;&#10; dwh-lnd &#61; &#34;dwh-lnd&#34;&#10; dwh-cur &#61; &#34;dwh-cur&#34;&#10; dwh-conf &#61; &#34;dwh-conf&#34;&#10; common &#61; &#34;cmn&#34;&#10; exp &#61; &#34;exp&#34;&#10; &#125;&#10; &#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [composer_config](variables.tf#L17) | Cloud Composer config. | <code title="object&#40;&#123;&#10; disable_deployment &#61; optional&#40;bool&#41;&#10; environment_size &#61; optional&#40;string, &#34;ENVIRONMENT_SIZE_SMALL&#34;&#41;&#10; software_config &#61; optional&#40;&#10; object&#40;&#123;&#10; airflow_config_overrides &#61; optional&#40;any&#41;&#10; pypi_packages &#61; optional&#40;any&#41;&#10; env_variables &#61; optional&#40;map&#40;string&#41;&#41;&#10; image_version &#61; string&#10; &#125;&#41;,&#10; &#123; image_version &#61; &#34;composer-2-airflow-2&#34; &#125;&#10; &#41;&#10; workloads_config &#61; optional&#40;&#10; object&#40;&#123;&#10; scheduler &#61; optional&#40;&#10; object&#40;&#123;&#10; cpu &#61; number&#10; memory_gb &#61; number&#10; storage_gb &#61; number&#10; count &#61; number&#10; &#125;&#41;,&#10; &#123;&#10; cpu &#61; 0.5&#10; memory_gb &#61; 1.875&#10; storage_gb &#61; 1&#10; count &#61; 1&#10; &#125;&#10; &#41;&#10; web_server &#61; optional&#40;&#10; object&#40;&#123;&#10; cpu &#61; number&#10; memory_gb &#61; number&#10; storage_gb &#61; number&#10; &#125;&#41;,&#10; &#123;&#10; cpu &#61; 0.5&#10; memory_gb &#61; 1.875&#10; storage_gb &#61; 1&#10; &#125;&#10; &#41;&#10; worker &#61; optional&#40;&#10; object&#40;&#123;&#10; cpu &#61; number&#10; memory_gb &#61; number&#10; storage_gb &#61; number&#10; min_count &#61; number&#10; max_count &#61; number&#10; &#125;&#41;,&#10; &#123;&#10; cpu &#61; 0.5&#10; memory_gb &#61; 1.875&#10; storage_gb &#61; 1&#10; min_count &#61; 1&#10; max_count &#61; 3&#10; &#125;&#10; &#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; environment_size &#61; &#34;ENVIRONMENT_SIZE_SMALL&#34;&#10; software_config &#61; &#123;&#10; image_version &#61; &#34;composer-2-airflow-2&#34;&#10; &#125;&#10; workloads_config &#61; &#123;&#10; scheduler &#61; &#123;&#10; cpu &#61; 0.5&#10; memory_gb &#61; 1.875&#10; storage_gb &#61; 1&#10; count &#61; 1&#10; &#125;&#10; web_server &#61; &#123;&#10; cpu &#61; 0.5&#10; memory_gb &#61; 1.875&#10; storage_gb &#61; 1&#10; &#125;&#10; worker &#61; &#123;&#10; cpu &#61; 0.5&#10; memory_gb &#61; 1.875&#10; storage_gb &#61; 1&#10; min_count &#61; 1&#10; max_count &#61; 3&#10; &#125;&#10; &#125;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [data_catalog_tags](variables.tf#L105) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | <code title="map&#40;object&#40;&#123;&#10; description &#61; optional&#40;string&#41;&#10; iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code title="&#123;&#10; &#34;3_Confidential&#34; &#61; &#123;&#125;&#10; &#34;2_Private&#34; &#61; &#123;&#125;&#10; &#34;1_Sensitive&#34; &#61; &#123;&#125;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [data_force_destroy](variables.tf#L119) | Flag to set 'force_destroy' on data services like BiguQery or Cloud Storage. | <code>bool</code> | | <code>false</code> |
| [groups](variables.tf#L125) | User groups. | <code>map&#40;string&#41;</code> | | <code title="&#123;&#10; data-analysts &#61; &#34;gcp-data-analysts&#34;&#10; data-engineers &#61; &#34;gcp-data-engineers&#34;&#10; data-security &#61; &#34;gcp-data-security&#34;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [location](variables.tf#L135) | Location used for multi-regional resources. | <code>string</code> | | <code>&#34;eu&#34;</code> |
| [network_config](variables.tf#L141) | Shared VPC network configurations to use. If null networks will be created in projects with preconfigured values. | <code title="object&#40;&#123;&#10; host_project &#61; string&#10; network_self_link &#61; string&#10; subnet_self_links &#61; object&#40;&#123;&#10; load &#61; string&#10; transformation &#61; string&#10; orchestration &#61; string&#10; &#125;&#41;&#10; composer_ip_ranges &#61; object&#40;&#123;&#10; cloudsql &#61; string&#10; gke_master &#61; string&#10; &#125;&#41;&#10; composer_secondary_ranges &#61; object&#40;&#123;&#10; pods &#61; string&#10; services &#61; string&#10; &#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [project_services](variables.tf#L212) | List of core services enabled on all projects. | <code>list&#40;string&#41;</code> | | <code title="&#91;&#10; &#34;cloudresourcemanager.googleapis.com&#34;,&#10; &#34;iam.googleapis.com&#34;,&#10; &#34;serviceusage.googleapis.com&#34;,&#10; &#34;stackdriver.googleapis.com&#34;&#10;&#93;">&#91;&#8230;&#93;</code> |
| [project_suffix](variables.tf#L223) | Suffix used only for project ids. | <code>string</code> | | <code>null</code> |
| [region](variables.tf#L229) | Region used for regional resources. | <code>string</code> | | <code>&#34;europe-west1&#34;</code> |
| [service_encryption_keys](variables.tf#L235) | Cloud KMS to use to encrypt different services. Key location should match service region. | <code title="object&#40;&#123;&#10; bq &#61; string&#10; composer &#61; string&#10; dataflow &#61; string&#10; storage &#61; string&#10; pubsub &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
## Outputs
@ -296,18 +294,3 @@ Features to add in future releases:
- Add example on how to use Cloud Data Loss Prevention
- Add solution to handle Tables, Views, and Authorized Views lifecycle
- Add solution to handle Metadata lifecycle
## Test
```hcl
module "test" {
source = "./fabric/blueprints/data-solutions/data-platform-foundations/"
organization_domain = "example.com"
project_config = {
billing_account_id = "123456-123456-123456"
parent = "folders/12345678"
}
prefix = "prefix"
}
# tftest modules=43 resources=285
```

View File

@ -0,0 +1,37 @@
/**
* Copyright 2023 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 {
_drp_iam = flatten([
for principal, roles in local.drp_iam : [
for role in roles : {
key = "${principal}-${role}"
principal = principal
role = role
}
]
])
drp_iam_additive = {
for binding in local._drp_iam : binding.key => {
role = binding.role
member = local.iam_principals[binding.principal]
}
}
drp_iam_auth = {
for binding in local._drp_iam :
binding.role => local.iam_principals[binding.principal]...
}
}

View File

@ -0,0 +1,47 @@
/**
* Copyright 2023 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 {
_load_iam = flatten([
for principal, roles in local.load_iam : [
for role in roles : {
key = "${principal}-${role}"
principal = principal
role = role
}
]
])
load_iam_additive = {
for binding in local._load_iam : binding.key => {
role = binding.role
member = local.iam_principals[binding.principal]
}
}
load_iam_auth = {
for binding in local._load_iam :
binding.role => local.iam_principals[binding.principal]...
}
load_subnet = (
local.use_shared_vpc
? var.network_config.subnet_self_links.orchestration
: values(module.load-vpc.0.subnet_self_links)[0]
)
load_vpc = (
local.use_shared_vpc
? var.network_config.network_self_link
: module.load-vpc.0.self_link
)
}

View File

@ -0,0 +1,50 @@
/**
* Copyright 2023 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 {
_orch_iam = flatten([
for principal, roles in local.orch_iam : [
for role in roles : {
key = "${principal}-${role}"
principal = principal
role = role
}
]
])
orch_iam_additive = {
for binding in local._orch_iam : binding.key => {
role = binding.role
member = local.iam_principals[binding.principal]
}
}
orch_iam_auth = {
for binding in local._orch_iam :
binding.role => local.iam_principals[binding.principal]...
}
orch_subnet = (
local.use_shared_vpc
? var.network_config.subnet_self_links.orchestration
: values(module.orch-vpc.0.subnet_self_links)[0]
)
orch_vpc = (
local.use_shared_vpc
? var.network_config.network_self_link
: module.orch-vpc.0.self_link
)
# TODO: use new artifact registry module output
orch_docker_path = format("%s-docker.pkg.dev/%s/%s",
var.region, module.orch-project.project_id, module.orch-artifact-reg.name)
}

View File

@ -0,0 +1,47 @@
/**
* Copyright 2023 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 {
_trf_iam = flatten([
for principal, roles in local.trf_iam : [
for role in roles : {
key = "${principal}-${role}"
principal = principal
role = role
}
]
])
trf_iam_additive = {
for binding in local._trf_iam : binding.key => {
role = binding.role
member = local.iam_principals[binding.principal]
}
}
trf_iam_auth = {
for binding in local._trf_iam :
binding.role => local.iam_principals[binding.principal]...
}
transf_subnet = (
local.use_shared_vpc
? var.network_config.subnet_self_links.orchestration
: values(module.transf-vpc.0.subnet_self_links)[0]
)
transf_vpc = (
local.use_shared_vpc
? var.network_config.network_self_link
: module.transf-vpc.0.self_link
)
}

View File

@ -0,0 +1,68 @@
/**
* Copyright 2023 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 {
_dwh_iam = flatten([
for principal, roles in local.dwh_iam : [
for role in roles : {
key = "${principal}-${role}"
principal = principal
role = role
}
]
])
_lnd_iam = flatten([
for principal, roles in local.lnd_iam : [
for role in roles : {
key = "${principal}-${role}"
principal = principal
role = role
}
]
])
dwh_iam_additive = {
for binding in local._dwh_iam : binding.key => {
role = binding.role
member = local.iam_principals[binding.principal]
}
}
dwh_iam_auth = {
for binding in local._dwh_iam :
binding.role => local.iam_principals[binding.principal]...
}
dwh_services = concat(var.project_services, [
"bigquery.googleapis.com",
"bigqueryreservation.googleapis.com",
"bigquerystorage.googleapis.com",
"cloudkms.googleapis.com",
"compute.googleapis.com",
"dataflow.googleapis.com",
"pubsub.googleapis.com",
"servicenetworking.googleapis.com",
"storage.googleapis.com",
"storage-component.googleapis.com"
])
lnd_iam_additive = {
for binding in local._lnd_iam : binding.key => {
role = binding.role
member = local.iam_principals[binding.principal]
}
}
lnd_iam_auth = {
for binding in local._lnd_iam :
binding.role => local.iam_principals[binding.principal]...
}
}

View File

@ -0,0 +1,37 @@
/**
* Copyright 2023 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 {
_cmn_iam = flatten([
for principal, roles in local.cmn_iam : [
for role in roles : {
key = "${principal}-${role}"
principal = principal
role = role
}
]
])
cmn_iam_additive = {
for binding in local._cmn_iam : binding.key => {
role = binding.role
member = local.iam_principals[binding.principal]
}
}
cmn_iam_auth = {
for binding in local._cmn_iam :
binding.role => local.iam_principals[binding.principal]...
}
}

View File

@ -35,6 +35,22 @@ locals {
groups_iam = {
for k, v in local.groups : k => "group:${v}"
}
iam_principals = {
data_analysts = "group:${local.groups.data-analysts}"
data_engineers = "group:${local.groups.data-engineers}"
data_security = "group:${local.groups.data-security}"
robots_cloudbuild = "serviceAccount:${module.orch-project.service_accounts.robots.cloudbuild}"
robots_composer = "serviceAccount:${module.orch-project.service_accounts.robots.composer}"
robots_dataflow_load = "serviceAccount:${module.load-project.service_accounts.robots.dataflow}"
robots_dataflow_trf = "serviceAccount:${module.transf-project.service_accounts.robots.dataflow}"
sa_drop_bq = module.drop-sa-bq-0.iam_email
sa_drop_cs = module.drop-sa-cs-0.iam_email
sa_drop_ps = module.drop-sa-ps-0.iam_email
sa_load = module.load-sa-df-0.iam_email
sa_orch = module.orch-sa-cmp-0.iam_email
sa_transf_bq = module.transf-sa-bq-0.iam_email,
sa_transf_df = module.transf-sa-df-0.iam_email,
}
project_suffix = var.project_suffix == null ? "" : "-${var.project_suffix}"
service_encryption_keys = var.service_encryption_keys
shared_vpc_project = try(var.network_config.host_project, null)
@ -57,6 +73,7 @@ locals {
]
]) : "${binding.role}-${binding.member}" => binding
}
use_projects = var.project_config.billing_account_id == null
use_shared_vpc = var.network_config != null
}

View File

@ -19,54 +19,59 @@ variable "composer_config" {
type = object({
disable_deployment = optional(bool)
environment_size = optional(string, "ENVIRONMENT_SIZE_SMALL")
software_config = optional(object({
airflow_config_overrides = optional(any)
pypi_packages = optional(any)
env_variables = optional(map(string))
image_version = string
}), {
image_version = "composer-2-airflow-2"
})
workloads_config = optional(object({
scheduler = optional(object(
{
cpu = number
memory_gb = number
storage_gb = number
count = number
}
), {
cpu = 0.5
memory_gb = 1.875
storage_gb = 1
count = 1
})
web_server = optional(object(
{
cpu = number
memory_gb = number
storage_gb = number
}
), {
cpu = 0.5
memory_gb = 1.875
storage_gb = 1
})
worker = optional(object(
{
cpu = number
memory_gb = number
storage_gb = number
min_count = number
max_count = number
}
), {
cpu = 0.5
memory_gb = 1.875
storage_gb = 1
min_count = 1
max_count = 3
})
software_config = optional(
object({
airflow_config_overrides = optional(any)
pypi_packages = optional(any)
env_variables = optional(map(string))
image_version = string
}),
{ image_version = "composer-2-airflow-2" }
)
workloads_config = optional(
object({
scheduler = optional(
object({
cpu = number
memory_gb = number
storage_gb = number
count = number
}),
{
cpu = 0.5
memory_gb = 1.875
storage_gb = 1
count = 1
}
)
web_server = optional(
object({
cpu = number
memory_gb = number
storage_gb = number
}),
{
cpu = 0.5
memory_gb = 1.875
storage_gb = 1
}
)
worker = optional(
object({
cpu = number
memory_gb = number
storage_gb = number
min_count = number
max_count = number
}),
{
cpu = 0.5
memory_gb = 1.875
storage_gb = 1
min_count = 1
max_count = 3
}
)
}))
})
default = {

View File

@ -16,9 +16,26 @@
locals {
iam_lnd = {
"roles/storage.objectCreator" = [module.land-sa-0.iam_email]
"roles/storage.objectViewer" = [module.processing-sa-cmp-0.iam_email]
"roles/storage.objectAdmin" = [module.processing-sa-0.iam_email]
"roles/storage.objectCreator" = [
module.land-sa-0.iam_email
]
"roles/storage.objectViewer" = [
module.processing-sa-cmp-0.iam_email
]
"roles/storage.objectAdmin" = [
module.processing-sa-0.iam_email
]
}
# this only works because the service account module uses a static output
iam_lnd_additive = {
for k in flatten([
for role, members in local.iam_lnd : [
for member in members : {
role = role
member = member
}
]
]) : "${k.member}-${k.role}" => k
}
}
@ -27,14 +44,20 @@ module "land-project" {
parent = var.project_config.parent
billing_account = var.project_config.billing_account_id
project_create = var.project_config.billing_account_id != null
prefix = var.project_config.billing_account_id == null ? null : var.prefix
prefix = (
var.project_config.billing_account_id == null ? null : var.prefix
)
name = (
var.project_config.billing_account_id == null
? var.project_config.project_ids.landing
: "${var.project_config.project_ids.landing}${local.project_suffix}"
)
iam = var.project_config.billing_account_id != null ? local.iam_lnd : null
iam_additive = var.project_config.billing_account_id == null ? local.iam_lnd : null
iam = (
var.project_config.billing_account_id == null ? {} : local.iam_lnd
)
iam_bindings_additive = (
var.project_config.billing_account_id != null ? {} : local.iam_lnd_additive
)
services = [
"bigquery.googleapis.com",
"bigqueryreservation.googleapis.com",

View File

@ -15,15 +15,23 @@
# tfdoc:file:description Processing project and VPC.
locals {
iam_processing = {
iam_prc = {
"roles/bigquery.jobUser" = [
module.processing-sa-cmp-0.iam_email,
module.processing-sa-0.iam_email
]
"roles/composer.admin" = [local.groups_iam.data-engineers]
"roles/dataflow.admin" = [module.processing-sa-cmp-0.iam_email]
"roles/dataflow.worker" = [module.processing-sa-0.iam_email]
"roles/composer.environmentAndStorageObjectAdmin" = [local.groups_iam.data-engineers]
"roles/composer.admin" = [
local.groups_iam.data-engineers
]
"roles/dataflow.admin" = [
module.processing-sa-cmp-0.iam_email
]
"roles/dataflow.worker" = [
module.processing-sa-0.iam_email
]
"roles/composer.environmentAndStorageObjectAdmin" = [
local.groups_iam.data-engineers
]
"roles/composer.ServiceAgentV2Ext" = [
"serviceAccount:${module.processing-project.service_accounts.robots.composer}"
]
@ -37,20 +45,39 @@ locals {
module.processing-sa-0.iam_email
]
"roles/iam.serviceAccountUser" = [
module.processing-sa-cmp-0.iam_email, local.groups_iam.data-engineers
module.processing-sa-cmp-0.iam_email,
local.groups_iam.data-engineers
]
"roles/iap.httpsResourceAccessor" = [
local.groups_iam.data-engineers
]
"roles/serviceusage.serviceUsageConsumer" = [
local.groups_iam.data-engineers
]
"roles/iap.httpsResourceAccessor" = [local.groups_iam.data-engineers]
"roles/serviceusage.serviceUsageConsumer" = [local.groups_iam.data-engineers]
"roles/storage.admin" = [
module.processing-sa-cmp-0.iam_email,
"serviceAccount:${module.processing-project.service_accounts.robots.composer}",
local.groups_iam.data-engineers
]
}
# this only works because the service account module uses a static output
iam_prc_additive = {
for k in flatten([
for role, members in local.iam_prc : [
for member in members : {
role = role
member = member
}
]
]) : "${k.member}-${k.role}" => k
}
processing_subnet = (
local.use_shared_vpc
? var.network_config.subnet_self_link
: try(module.processing-vpc.0.subnet_self_links["${var.region}/${var.prefix}-processing"], null)
: try(
module.processing-vpc.0.subnet_self_links["${var.region}/${var.prefix}-processing"],
null
)
)
processing_vpc = (
local.use_shared_vpc
@ -64,15 +91,23 @@ module "processing-project" {
parent = var.project_config.parent
billing_account = var.project_config.billing_account_id
project_create = var.project_config.billing_account_id != null
prefix = var.project_config.billing_account_id == null ? null : var.prefix
prefix = (
var.project_config.billing_account_id == null ? null : var.prefix
)
name = (
var.project_config.billing_account_id == null
? var.project_config.project_ids.processing
: "${var.project_config.project_ids.processing}${local.project_suffix}"
)
iam = var.project_config.billing_account_id != null ? local.iam_processing : null
iam_additive = var.project_config.billing_account_id == null ? local.iam_processing : null
oslogin = false
iam = (
var.project_config.billing_account_id == null ? {} : local.iam_prc
)
iam_bindings_additive = (
var.project_config.billing_account_id != null ? {} : local.iam_prc_additive
)
compute_metadata = {
enable-oslogin = "false"
}
services = [
"bigquery.googleapis.com",
"bigqueryreservation.googleapis.com",

View File

@ -15,15 +15,32 @@
# tfdoc:file:description Data curated project and resources.
locals {
cur_iam = {
"roles/bigquery.dataOwner" = [module.processing-sa-0.iam_email]
cur_services = [
"bigquery.googleapis.com",
"bigqueryreservation.googleapis.com",
"bigquerystorage.googleapis.com",
"cloudkms.googleapis.com",
"cloudresourcemanager.googleapis.com",
"compute.googleapis.com",
"iam.googleapis.com",
"servicenetworking.googleapis.com",
"serviceusage.googleapis.com",
"stackdriver.googleapis.com",
"storage.googleapis.com",
"storage-component.googleapis.com"
]
iam_cur = {
"roles/bigquery.dataOwner" = [
module.processing-sa-0.iam_email
]
"roles/bigquery.dataViewer" = [
module.cur-sa-0.iam_email,
local.groups_iam.data-analysts,
local.groups_iam.data-engineers
]
"roles/bigquery.jobUser" = [
module.processing-sa-0.iam_email, # Remove once bug is fixed. https://github.com/apache/airflow/issues/32106
# Remove once bug is fixed. https://github.com/apache/airflow/issues/32106
module.processing-sa-0.iam_email,
module.cur-sa-0.iam_email,
local.groups_iam.data-analysts,
local.groups_iam.data-engineers
@ -43,22 +60,21 @@ locals {
local.groups_iam.data-analysts,
local.groups_iam.data-engineers
]
"roles/storage.objectAdmin" = [module.processing-sa-0.iam_email]
"roles/storage.objectAdmin" = [
module.processing-sa-0.iam_email
]
}
# this only works because the service account module uses a static output
iam_cur_additive = {
for k in flatten([
for role, members in local.iam_cur : [
for member in members : {
role = role
member = member
}
]
]) : "${k.member}-${k.role}" => k
}
cur_services = [
"bigquery.googleapis.com",
"bigqueryreservation.googleapis.com",
"bigquerystorage.googleapis.com",
"cloudkms.googleapis.com",
"cloudresourcemanager.googleapis.com",
"compute.googleapis.com",
"iam.googleapis.com",
"servicenetworking.googleapis.com",
"serviceusage.googleapis.com",
"stackdriver.googleapis.com",
"storage.googleapis.com",
"storage-component.googleapis.com"
]
}
# Project
@ -68,15 +84,21 @@ module "cur-project" {
parent = var.project_config.parent
billing_account = var.project_config.billing_account_id
project_create = var.project_config.billing_account_id != null
prefix = var.project_config.billing_account_id == null ? null : var.prefix
prefix = (
var.project_config.billing_account_id == null ? null : var.prefix
)
name = (
var.project_config.billing_account_id == null
? var.project_config.project_ids.curated
: "${var.project_config.project_ids.curated}${local.project_suffix}"
)
iam = var.project_config.billing_account_id != null ? local.cur_iam : {}
iam_additive = var.project_config.billing_account_id == null ? local.cur_iam : {}
services = local.cur_services
iam = (
var.project_config.billing_account_id != null ? {} : local.iam_cur
)
iam_bindings_additive = (
var.project_config.billing_account_id == null ? {} : local.iam_cur_additive
)
services = local.cur_services
service_encryption_key_ids = {
bq = [var.service_encryption_keys.bq]
storage = [var.service_encryption_keys.storage]

View File

@ -15,15 +15,23 @@
# tfdoc:file:description Common project and resources.
locals {
iam_common = {
"roles/dlp.admin" = [local.groups_iam.data-security]
"roles/dlp.estimatesAdmin" = [local.groups_iam.data-engineers]
"roles/dlp.reader" = [local.groups_iam.data-engineers]
iam_cmn = {
"roles/dlp.admin" = [
local.groups_iam.data-security
]
"roles/dlp.estimatesAdmin" = [
local.groups_iam.data-engineers
]
"roles/dlp.reader" = [
local.groups_iam.data-engineers
]
"roles/dlp.user" = [
module.processing-sa-0.iam_email,
local.groups_iam.data-engineers
]
"roles/datacatalog.admin" = [local.groups_iam.data-security]
"roles/datacatalog.admin" = [
local.groups_iam.data-security
]
"roles/datacatalog.viewer" = [
module.processing-sa-0.iam_email,
local.groups_iam.data-analysts
@ -32,20 +40,37 @@ locals {
module.processing-sa-0.iam_email
]
}
# this only works because the service account module uses a static output
iam_cmn_additive = {
for k in flatten([
for role, members in local.iam_cmn : [
for member in members : {
role = role
member = member
}
]
]) : "${k.member}-${k.role}" => k
}
}
module "common-project" {
source = "../../../modules/project"
parent = var.project_config.parent
billing_account = var.project_config.billing_account_id
project_create = var.project_config.billing_account_id != null
prefix = var.project_config.billing_account_id == null ? null : var.prefix
prefix = (
var.project_config.billing_account_id == null ? null : var.prefix
)
name = (
var.project_config.billing_account_id == null
? var.project_config.project_ids.common
: "${var.project_config.project_ids.common}${local.project_suffix}"
)
iam = var.project_config.billing_account_id != null ? local.iam_common : null
iam_additive = var.project_config.billing_account_id == null ? local.iam_common : null
iam = (
var.project_config.billing_account_id == null ? {} : local.iam_cmn
)
iam_bindings_additive = (
var.project_config.billing_account_id != null ? {} : local.iam_cmn_additive
)
services = [
"cloudresourcemanager.googleapis.com",
"datacatalog.googleapis.com",

View File

@ -12,6 +12,32 @@ The following diagram is a high-level reference of the resources created and man
A set of demo [Airflow pipelines](./demo/) are also part of this blueprint: they can be run on top of the foundational infrastructure to verify and test the setup.
<!-- BEGIN TOC -->
- [Design overview and choices](#design-overview-and-choices)
- [Project structure](#project-structure)
- [Roles](#roles)
- [Service accounts](#service-accounts)
- [User groups](#user-groups)
- [Virtual Private Cloud (VPC) design](#virtual-private-cloud-vpc-design)
- [IP ranges and subnetting](#ip-ranges-and-subnetting)
- [Resource naming conventions](#resource-naming-conventions)
- [Encryption](#encryption)
- [Data Anonymization](#data-anonymization)
- [Data Catalog](#data-catalog)
- [How to run this script](#how-to-run-this-script)
- [Variable configuration](#variable-configuration)
- [How to use this blueprint from Terraform](#how-to-use-this-blueprint-from-terraform)
- [Customizations](#customizations)
- [Assign roles at BQ Dataset level](#assign-roles-at-bq-dataset-level)
- [Project Configuration](#project-configuration)
- [Shared VPC](#shared-vpc)
- [Customer Managed Encryption key](#customer-managed-encryption-key)
- [Demo pipeline](#demo-pipeline)
- [Files](#files)
- [Variables](#variables)
- [Outputs](#outputs)
<!-- END TOC -->
## Design overview and choices
Despite its simplicity, this stage implements the basics of a design that we've seen working well for various customers.
@ -203,7 +229,7 @@ module "data-platform" {
prefix = "myprefix"
}
# tftest modules=23 resources=123
# tftest modules=23 resources=135
```
## Customizations

View File

@ -14,16 +14,6 @@
locals {
iam = {
"roles/iam.serviceAccountUser" = [
module.service-account-orch.iam_email
]
"roles/iam.serviceAccountTokenCreator" = var.data_eng_principals
# GCS roles
"roles/storage.objectAdmin" = [
module.service-account-df.iam_email,
module.service-account-landing.iam_email
]
# BigQuery roles
"roles/bigquery.admin" = var.data_eng_principals
"roles/bigquery.dataOwner" = [
module.service-account-df.iam_email
@ -34,9 +24,7 @@ locals {
"roles/bigquery.jobUser" = [
module.service-account-bq.iam_email
]
# Compute
"roles/compute.viewer" = var.data_eng_principals
# Dataflow roles
"roles/dataflow.admin" = concat(
[module.service-account-orch.iam_email],
var.data_eng_principals
@ -45,6 +33,25 @@ locals {
"roles/dataflow.worker" = [
module.service-account-df.iam_email,
]
"roles/iam.serviceAccountUser" = [
module.service-account-orch.iam_email
]
"roles/iam.serviceAccountTokenCreator" = var.data_eng_principals
"roles/storage.objectAdmin" = [
module.service-account-df.iam_email,
module.service-account-landing.iam_email
]
}
# this only works because the service account module uses a static output
iam_additive = {
for k in flatten([
for role, members in local.iam : [
for member in members : {
role = role
member = member
}
]
]) : "${k.member}-${k.role}" => k
}
network_subnet_selflink = try(
module.vpc[0].subnets["${var.region}/subnet"].self_link,
@ -75,8 +82,12 @@ module "project" {
"storage.googleapis.com",
"storage-component.googleapis.com",
]
iam = var.project_config.billing_account_id != null ? local.iam : {}
iam_additive = var.project_config.billing_account_id == null ? local.iam : {}
iam = (
var.project_config.billing_account_id != null ? local.iam : {}
)
iam_bindings_additive = (
var.project_config.billing_account_id == null ? local.iam_additive : {}
)
shared_vpc_service_config = var.network_config.host_project == null ? null : {
attach = true
host_project = var.network_config.host_project

View File

@ -153,25 +153,24 @@ terraform init
terraform apply
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [access_policy_config](variables.tf#L17) | Provide 'access_policy_create' values if a folder scoped Access Policy creation is needed, uses existing 'policy_name' otherwise. Parent is in 'organizations/123456' format. Policy will be created scoped to the folder. | <code title="object&#40;&#123;&#10; policy_name &#61; optional&#40;string, null&#41;&#10; access_policy_create &#61; optional&#40;object&#40;&#123;&#10; parent &#61; string&#10; title &#61; string&#10; &#125;&#41;, null&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [folder_config](variables.tf#L49) | Provide 'folder_create' values if folder creation is needed, uses existing 'folder_id' otherwise. Parent is in 'folders/nnn' or 'organizations/nnn' format. | <code title="object&#40;&#123;&#10; folder_id &#61; optional&#40;string, null&#41;&#10; folder_create &#61; optional&#40;object&#40;&#123;&#10; display_name &#61; string&#10; parent &#61; string&#10; &#125;&#41;, null&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [organization](variables.tf#L128) | Organization details. | <code title="object&#40;&#123;&#10; domain &#61; string&#10; id &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [prefix](variables.tf#L136) | Prefix used for resources that need unique names. | <code>string</code> | ✓ | |
| [project_config](variables.tf#L141) | Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | <code title="object&#40;&#123;&#10; billing_account_id &#61; optional&#40;string, null&#41;&#10; project_ids &#61; optional&#40;object&#40;&#123;&#10; sec-core &#61; string&#10; audit-logs &#61; string&#10; &#125;&#41;, &#123;&#10; sec-core &#61; &#34;sec-core&#34;&#10; audit-logs &#61; &#34;audit-logs&#34;&#10; &#125;&#10; &#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [organization](variables.tf#L129) | Organization details. | <code title="object&#40;&#123;&#10; domain &#61; string&#10; id &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [prefix](variables.tf#L137) | Prefix used for resources that need unique names. | <code>string</code> | ✓ | |
| [project_config](variables.tf#L142) | Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | <code title="object&#40;&#123;&#10; billing_account_id &#61; optional&#40;string, null&#41;&#10; project_ids &#61; optional&#40;object&#40;&#123;&#10; sec-core &#61; string&#10; audit-logs &#61; string&#10; &#125;&#41;, &#123;&#10; sec-core &#61; &#34;sec-core&#34;&#10; audit-logs &#61; &#34;audit-logs&#34;&#10; &#125;&#10; &#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [data_dir](variables.tf#L29) | Relative path for the folder storing configuration data. | <code>string</code> | | <code>&#34;data&#34;</code> |
| [enable_features](variables.tf#L35) | Flag to enable features on the solution. | <code title="object&#40;&#123;&#10; encryption &#61; optional&#40;bool, false&#41;&#10; log_sink &#61; optional&#40;bool, true&#41;&#10; vpc_sc &#61; optional&#40;bool, true&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; encryption &#61; false&#10; log_sink &#61; true&#10; vpc_sc &#61; true&#10;&#125;">&#123;&#8230;&#125;</code> |
| [groups](variables.tf#L65) | User groups. | <code title="object&#40;&#123;&#10; workload-engineers &#61; optional&#40;string, &#34;gcp-data-engineers&#34;&#41;&#10; workload-security &#61; optional&#40;string, &#34;gcp-data-security&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [kms_keys](variables.tf#L75) | KMS keys to create, keyed by name. | <code title="map&#40;object&#40;&#123;&#10; iam &#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; locations &#61; optional&#40;list&#40;string&#41;, &#91;&#34;global&#34;, &#34;europe&#34;, &#34;europe-west1&#34;&#93;&#41;&#10; rotation_period &#61; optional&#40;string, &#34;7776000s&#34;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [log_locations](variables.tf#L86) | Optional locations for GCS, BigQuery, and logging buckets created here. | <code title="object&#40;&#123;&#10; bq &#61; optional&#40;string, &#34;europe&#34;&#41;&#10; storage &#61; optional&#40;string, &#34;europe&#34;&#41;&#10; logging &#61; optional&#40;string, &#34;global&#34;&#41;&#10; pubsub &#61; optional&#40;string, &#34;global&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; bq &#61; &#34;europe&#34;&#10; storage &#61; &#34;europe&#34;&#10; logging &#61; &#34;global&#34;&#10; pubsub &#61; null&#10;&#125;">&#123;&#8230;&#125;</code> |
| [log_sinks](variables.tf#L103) | Org-level log sinks, in name => {type, filter} format. | <code title="map&#40;object&#40;&#123;&#10; filter &#61; string&#10; type &#61; string&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code title="&#123;&#10; audit-logs &#61; &#123;&#10; filter &#61; &#34;logName:&#92;&#34;&#47;logs&#47;cloudaudit.googleapis.com&#37;2Factivity&#92;&#34; OR logName:&#92;&#34;&#47;logs&#47;cloudaudit.googleapis.com&#37;2Fsystem_event&#92;&#34;&#34;&#10; type &#61; &#34;bigquery&#34;&#10; &#125;&#10; vpc-sc &#61; &#123;&#10; filter &#61; &#34;protoPayload.metadata.&#64;type&#61;&#92;&#34;type.googleapis.com&#47;google.cloud.audit.VpcServiceControlAuditMetadata&#92;&#34;&#34;&#10; type &#61; &#34;bigquery&#34;&#10; &#125;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [vpc_sc_access_levels](variables.tf#L161) | VPC SC access level definitions. | <code title="map&#40;object&#40;&#123;&#10; combining_function &#61; optional&#40;string&#41;&#10; conditions &#61; optional&#40;list&#40;object&#40;&#123;&#10; device_policy &#61; optional&#40;object&#40;&#123;&#10; allowed_device_management_levels &#61; optional&#40;list&#40;string&#41;&#41;&#10; allowed_encryption_statuses &#61; optional&#40;list&#40;string&#41;&#41;&#10; require_admin_approval &#61; bool&#10; require_corp_owned &#61; bool&#10; require_screen_lock &#61; optional&#40;bool&#41;&#10; os_constraints &#61; optional&#40;list&#40;object&#40;&#123;&#10; os_type &#61; string&#10; minimum_version &#61; optional&#40;string&#41;&#10; require_verified_chrome_os &#61; optional&#40;bool&#41;&#10; &#125;&#41;&#41;&#41;&#10; &#125;&#41;&#41;&#10; ip_subnetworks &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; members &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; negate &#61; optional&#40;bool&#41;&#10; regions &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; required_access_levels &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10; description &#61; optional&#40;string&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [vpc_sc_egress_policies](variables.tf#L190) | VPC SC egress policy definitions. | <code title="map&#40;object&#40;&#123;&#10; from &#61; object&#40;&#123;&#10; identity_type &#61; optional&#40;string, &#34;ANY_IDENTITY&#34;&#41;&#10; identities &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#10; to &#61; object&#40;&#123;&#10; operations &#61; optional&#40;list&#40;object&#40;&#123;&#10; method_selectors &#61; optional&#40;list&#40;string&#41;&#41;&#10; service_name &#61; string&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10; resources &#61; optional&#40;list&#40;string&#41;&#41;&#10; resource_type_external &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [vpc_sc_ingress_policies](variables.tf#L210) | VPC SC ingress policy definitions. | <code title="map&#40;object&#40;&#123;&#10; from &#61; object&#40;&#123;&#10; access_levels &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; identity_type &#61; optional&#40;string&#41;&#10; identities &#61; optional&#40;list&#40;string&#41;&#41;&#10; resources &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#10; to &#61; object&#40;&#123;&#10; operations &#61; optional&#40;list&#40;object&#40;&#123;&#10; method_selectors &#61; optional&#40;list&#40;string&#41;&#41;&#10; service_name &#61; string&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10; resources &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [kms_keys](variables.tf#L75) | KMS keys to create, keyed by name. | <code title="map&#40;object&#40;&#123;&#10; iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings_additive &#61; optional&#40;map&#40;map&#40;any&#41;&#41;, &#123;&#125;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; locations &#61; optional&#40;list&#40;string&#41;, &#91;&#34;global&#34;, &#34;europe&#34;, &#34;europe-west1&#34;&#93;&#41;&#10; rotation_period &#61; optional&#40;string, &#34;7776000s&#34;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [log_locations](variables.tf#L87) | Optional locations for GCS, BigQuery, and logging buckets created here. | <code title="object&#40;&#123;&#10; bq &#61; optional&#40;string, &#34;europe&#34;&#41;&#10; storage &#61; optional&#40;string, &#34;europe&#34;&#41;&#10; logging &#61; optional&#40;string, &#34;global&#34;&#41;&#10; pubsub &#61; optional&#40;string, &#34;global&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; bq &#61; &#34;europe&#34;&#10; storage &#61; &#34;europe&#34;&#10; logging &#61; &#34;global&#34;&#10; pubsub &#61; null&#10;&#125;">&#123;&#8230;&#125;</code> |
| [log_sinks](variables.tf#L104) | Org-level log sinks, in name => {type, filter} format. | <code title="map&#40;object&#40;&#123;&#10; filter &#61; string&#10; type &#61; string&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code title="&#123;&#10; audit-logs &#61; &#123;&#10; filter &#61; &#34;logName:&#92;&#34;&#47;logs&#47;cloudaudit.googleapis.com&#37;2Factivity&#92;&#34; OR logName:&#92;&#34;&#47;logs&#47;cloudaudit.googleapis.com&#37;2Fsystem_event&#92;&#34;&#34;&#10; type &#61; &#34;bigquery&#34;&#10; &#125;&#10; vpc-sc &#61; &#123;&#10; filter &#61; &#34;protoPayload.metadata.&#64;type&#61;&#92;&#34;type.googleapis.com&#47;google.cloud.audit.VpcServiceControlAuditMetadata&#92;&#34;&#34;&#10; type &#61; &#34;bigquery&#34;&#10; &#125;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [vpc_sc_access_levels](variables.tf#L162) | VPC SC access level definitions. | <code title="map&#40;object&#40;&#123;&#10; combining_function &#61; optional&#40;string&#41;&#10; conditions &#61; optional&#40;list&#40;object&#40;&#123;&#10; device_policy &#61; optional&#40;object&#40;&#123;&#10; allowed_device_management_levels &#61; optional&#40;list&#40;string&#41;&#41;&#10; allowed_encryption_statuses &#61; optional&#40;list&#40;string&#41;&#41;&#10; require_admin_approval &#61; bool&#10; require_corp_owned &#61; bool&#10; require_screen_lock &#61; optional&#40;bool&#41;&#10; os_constraints &#61; optional&#40;list&#40;object&#40;&#123;&#10; os_type &#61; string&#10; minimum_version &#61; optional&#40;string&#41;&#10; require_verified_chrome_os &#61; optional&#40;bool&#41;&#10; &#125;&#41;&#41;&#41;&#10; &#125;&#41;&#41;&#10; ip_subnetworks &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; members &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; negate &#61; optional&#40;bool&#41;&#10; regions &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; required_access_levels &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10; description &#61; optional&#40;string&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [vpc_sc_egress_policies](variables.tf#L191) | VPC SC egress policy definitions. | <code title="map&#40;object&#40;&#123;&#10; from &#61; object&#40;&#123;&#10; identity_type &#61; optional&#40;string, &#34;ANY_IDENTITY&#34;&#41;&#10; identities &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#10; to &#61; object&#40;&#123;&#10; operations &#61; optional&#40;list&#40;object&#40;&#123;&#10; method_selectors &#61; optional&#40;list&#40;string&#41;&#41;&#10; service_name &#61; string&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10; resources &#61; optional&#40;list&#40;string&#41;&#41;&#10; resource_type_external &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [vpc_sc_ingress_policies](variables.tf#L211) | VPC SC ingress policy definitions. | <code title="map&#40;object&#40;&#123;&#10; from &#61; object&#40;&#123;&#10; access_levels &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; identity_type &#61; optional&#40;string&#41;&#10; identities &#61; optional&#40;list&#40;string&#41;&#41;&#10; resources &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#10; to &#61; object&#40;&#123;&#10; operations &#61; optional&#40;list&#40;object&#40;&#123;&#10; method_selectors &#61; optional&#40;list&#40;string&#41;&#41;&#10; service_name &#61; string&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10; resources &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
## Outputs
@ -180,7 +179,6 @@ terraform apply
| [folders](outputs.tf#L15) | Folders id. | |
| [folders_sink_writer_identities](outputs.tf#L23) | Folders id. | |
| [kms_keys](outputs.tf#L31) | Cloud KMS encryption keys created. | |
<!-- END TFDOC -->
## Test

View File

@ -25,12 +25,9 @@ locals {
for k, v in var.kms_keys : k => v if contains(v.locations, loc)
}
}
kms_log_locations = distinct(flatten([
for k, v in local.kms_log_sink_keys : compact(v.locations)
]))
# Log sink keys
kms_log_sink_keys = {
"storage" = {
labels = {}
@ -61,8 +58,12 @@ module "sec-project" {
name = var.project_config.project_ids["sec-core"]
parent = module.folder.id
billing_account = var.project_config.billing_account_id
project_create = var.project_config.billing_account_id != null && var.enable_features.encryption
prefix = var.project_config.billing_account_id == null ? null : var.prefix
project_create = (
var.project_config.billing_account_id != null && var.enable_features.encryption
)
prefix = (
var.project_config.billing_account_id == null ? null : var.prefix
)
group_iam = {
(local.groups.workload-security) = [
"roles/editor"
@ -76,17 +77,23 @@ module "sec-project" {
}
module "sec-kms" {
for_each = var.enable_features.encryption ? toset(local.kms_locations) : toset([])
for_each = (
var.enable_features.encryption
? toset(local.kms_locations)
: toset([])
)
source = "../../../modules/kms"
project_id = module.sec-project[0].project_id
keyring = {
location = each.key
name = "sec-${each.key}"
}
# rename to `key_iam` to switch to authoritative bindings
key_iam_additive = {
key_iam = {
for k, v in local.kms_locations_keys[each.key] : k => v.iam
}
key_iam_bindings_additive = {
for k, v in local.kms_locations_keys[each.key] : k => v.iam_bindings_additive
}
keys = local.kms_locations_keys[each.key]
}

View File

@ -75,10 +75,11 @@ variable "groups" {
variable "kms_keys" {
description = "KMS keys to create, keyed by name."
type = map(object({
iam = optional(map(list(string)), {})
labels = optional(map(string), {})
locations = optional(list(string), ["global", "europe", "europe-west1"])
rotation_period = optional(string, "7776000s")
iam = optional(map(list(string)), {})
iam_bindings_additive = optional(map(map(any)), {})
labels = optional(map(string), {})
locations = optional(list(string), ["global", "europe", "europe-west1"])
rotation_period = optional(string, "7776000s")
}))
default = {}
}

View File

@ -73,9 +73,6 @@ module "project" {
"compute.googleapis.com",
"secretmanager.googleapis.com",
]
iam = {}
iam_additive = {}
shared_vpc_service_config = var.shared_vpc_project_id == null ? null : {
attach = true
host_project = var.shared_vpc_project_id

View File

@ -1,270 +1,95 @@
# Minimal Project Factory
# Project Factory
This module implements a minimal, opinionated project factory (see [Factories](../README.md) for rationale) that allows for the creation of projects.
This is a working example of how to manage project creation at scale, by wrapping the [project module](../../../modules/project/) and driving it via external data, either directly provided or parsed via YAML files.
While the module can be invoked by manually populating the required variables, its interface is meant for the massive creation of resources leveraging a set of well-defined YaML documents, as shown in the examples below.
The wrapping layer around the project module is intentionally thin, so that
The Project Factory is meant to be executed by a Service Account (or a regular user) having this minimal set of permissions over your resources:
- all the features of the project module are available
- no "magic" or hidden side effects are implemented in code
- debugging and integration of new features is simple
* **Org level** - a custom role for networking operations including the following permissions
* `"compute.organizations.enableXpnResource"`,
* `"compute.organizations.disableXpnResource"`,
* `"compute.subnetworks.setIamPolicy"`,
* `"dns.networks.bindPrivateDNSZone"`
* and role `"roles/orgpolicy.policyAdmin"`
* **on each folder** where projects will be created
* `"roles/logging.admin"`
* `"roles/owner"`
* `"roles/resourcemanager.folderAdmin"`
* `"roles/resourcemanager.projectCreator"`
* **on the host project** for the Shared VPC/s
* `"roles/browser"`
* `"roles/compute.viewer"`
* `"roles/dns.admin"`
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
- project creation on the nodes (folder or org) where projects will be defined
The module also supports optional creation of specific resources that usually part of the project creation flow:
- service accounts used for VM instances, and associated basic roles
- KMS key encrypt/decrypt permissions for service identities in the project
- membership in VPC SC standard or bridge perimeters
Compared to the previous version of this code, network-related resources (DNS zones, VPC subnets, etc.) have been removed as they are not typically in scope for the team who manages project creation, and adding them when needed requires just a few trivial code changes.
## Example
### Directory structure
```
.
├── data
│ ├── defaults.yaml
│ └── projects
│ ├── project-example-one.yaml
│ ├── project-example-two.yaml
│ └── project-example-three.yaml
├── main.tf
└── terraform.tfvars
```
### Terraform code
```hcl
locals {
defaults = yamldecode(file(local._defaults_file))
projects = {
for f in fileset("${local._data_dir}", "**/*.yaml") :
trimsuffix(f, ".yaml") => yamldecode(file("${local._data_dir}/${f}"))
module "project-factory" {
source = "./fabric/blueprints/factories/project-factory"
data_defaults = {
billing_account = "012345-67890A-ABCDEF"
}
data_merges = {
labels = {
environment = "test"
}
services = [
"stackdriver.googleapis.com"
]
}
data_overrides = {
contacts = {
"admin@example.com" = ["ALL"]
}
prefix = "test-pf"
}
factory_data = {
data_path = "data"
}
# these are usually set via variables
_base_dir = "./fabric/blueprints/factories/project-factory"
_data_dir = "${local._base_dir}/sample-data/projects/"
_defaults_file = "${local._base_dir}/sample-data/defaults.yaml"
}
module "projects" {
source = "./fabric/blueprints/factories/project-factory"
for_each = local.projects
defaults = local.defaults
project_id = each.key
descriptive_name = try(each.value.descriptive_name, null)
billing_account_id = try(each.value.billing_account_id, null)
billing_alert = try(each.value.billing_alert, null)
dns_zones = try(each.value.dns_zones, [])
essential_contacts = try(each.value.essential_contacts, [])
folder_id = each.value.folder_id
group_iam = try(each.value.group_iam, {})
iam = try(each.value.iam, {})
kms_service_agents = try(each.value.kms_service_agents, {})
labels = try(each.value.labels, {})
org_policies = try(each.value.org_policies, {})
prefix = each.value.prefix
service_accounts = try(each.value.service_accounts, {})
services = try(each.value.services, [])
service_identities_iam = try(each.value.service_identities_iam, {})
vpc = try(each.value.vpc, null)
}
# tftest modules=7 resources=38 inventory=example.yaml
```
### Projects configuration
```yaml
# ./data/defaults.yaml
# The following applies as overridable defaults for all projects
# All attributes are required
billing_account_id: 012345-67890A-BCDEF0
billing_alert:
amount: 1000
thresholds:
current: [0.5, 0.8]
forecasted: [0.5, 0.8]
credit_treatment: INCLUDE_ALL_CREDITS
environment_dns_zone: prod.gcp.example.com
essential_contacts: []
labels:
environment: production
department: legal
application: my-legal-bot
notification_channels: []
shared_vpc_self_link: https://www.googleapis.com/compute/v1/projects/project-example-host-project/global/networks/vpc-one
vpc_host_project: project-example-host-project
# tftest modules=6 resources=12 files=prj-app-1,prj-app-2 inventory=example.yaml
```
```yaml
# ./data/projects/project-example-one.yaml
# One file per project - projects will be named after the filename
# [opt] Billing account id - overrides default if set
billing_account_id: 012345-67890A-BCDEF0
# [opt] Billing alerts config - overrides default if set
billing_alert:
amount: 10
thresholds:
current:
- 0.5
- 0.8
forecasted: []
# [opt] DNS zones to be created as children of the environment_dns_zone defined in defaults
dns_zones:
- lorem
- ipsum
# [opt] Contacts for billing alerts and important notifications
essential_contacts:
- team-a-contacts@example.com
# Folder the project will be created as children of
folder_id: folders/012345678901
# [opt] Authoritative IAM bindings in group => [roles] format
group_iam:
test-team-foobar@fast-lab-0.gcp-pso-italy.net:
- roles/compute.admin
# [opt] Authoritative IAM bindings in role => [principals] format
# Generally used to grant roles to service accounts external to the project
iam:
roles/compute.admin:
- serviceAccount:service-account
# [opt] Service robots and keys they will be assigned as cryptoKeyEncrypterDecrypter
# in service => [keys] format
kms_service_agents:
compute: [key1, key2]
storage: [key1, key2]
# [opt] Labels for the project - merged with the ones defined in defaults
billing_account: 012345-67890A-BCDEF0
labels:
environment: prod
# [opt] Org policy overrides defined at project level
org_policies:
compute.disableGuestAttributesAccess:
rules:
- enforce: true
compute.trustedImageProjects:
rules:
- allow:
values:
- projects/fast-dev-iac-core-0
compute.vmExternalIpAccess:
rules:
- deny:
all: true
# [opt] Service account to create for the project and their roles on the project
# in name => [roles] format
service_accounts:
another-service-account:
- roles/compute.admin
my-service-account:
- roles/compute.admin
# [opt] IAM bindings on the service account resources.
# in name => {role => [members]} format
service_accounts_iam:
another-service-account:
- roles/iam.serviceAccountTokenCreator:
- group: app-team-1@example.com
# [opt] APIs to enable on the project.
app: app-1
team: foo
service_encryption_key_ids:
compute:
- projects/kms-central-prj/locations/europe-west3/keyRings/my-keyring/cryptoKeys/europe3-gce
services:
- storage.googleapis.com
- stackdriver.googleapis.com
- compute.googleapis.com
- storage.googleapis.com
service_accounts:
app-1-be: {}
app-1-fe: {}
# [opt] Roles to assign to the robots service accounts in robot => [roles] format
services_iam:
compute:
- roles/storage.objectViewer
# tftest-file id=prj-app-1 path=data/prj-app-1.yaml
```
# [opt] VPC setup.
# If set enables the `compute.googleapis.com` service and configures
# service project attachment
vpc:
```yaml
labels:
app: app-1
team: foo
service_accounts:
app-2-be: {}
# [opt] If set, enables the container API
gke_setup:
# Grants "roles/container.hostServiceAgentUser" to the container robot if set
enable_host_service_agent: false
# Grants "roles/compute.securityAdmin" to the container robot if set
enable_security_admin: true
# Host project the project will be service project of
host_project: fast-prod-net-spoke-0
# [opt] Services for which set up the IAM in the host project
service_iam_grants:
- dataproc.googleapis.com
# [opt] Roles to rant service project service identities in host project
service_identity_iam:
"roles/compute.networkUser":
- cloudservices
- container-engine
# [opt] Subnets in the host project where principals will be granted networkUser
# in region/subnet-name => [principals]
subnets_iam:
europe-west1/prod-default-ew1:
- user:foobar@example.com
- serviceAccount:service-account1@my-project.iam.gserviceaccount.com
# tftest-file id=prj-app-2 path=data/prj-app-2.yaml
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [billing_account_id](variables.tf#L17) | Billing account id. | <code>string</code> | ✓ | |
| [prefix](variables.tf#L144) | Prefix used for resource names. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L153) | Project id. | <code>string</code> | ✓ | |
| [billing_alert](variables.tf#L22) | Billing alert configuration. | <code title="object&#40;&#123;&#10; amount &#61; number&#10; thresholds &#61; object&#40;&#123;&#10; current &#61; list&#40;number&#41;&#10; forecasted &#61; list&#40;number&#41;&#10; &#125;&#41;&#10; credit_treatment &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [defaults](variables.tf#L35) | Project factory default values. | <code title="object&#40;&#123;&#10; billing_account_id &#61; string&#10; billing_alert &#61; object&#40;&#123;&#10; amount &#61; number&#10; thresholds &#61; object&#40;&#123;&#10; current &#61; list&#40;number&#41;&#10; forecasted &#61; list&#40;number&#41;&#10; &#125;&#41;&#10; credit_treatment &#61; string&#10; &#125;&#41;&#10; environment_dns_zone &#61; string&#10; essential_contacts &#61; list&#40;string&#41;&#10; labels &#61; map&#40;string&#41;&#10; notification_channels &#61; list&#40;string&#41;&#10; shared_vpc_self_link &#61; string&#10; vpc_host_project &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [descriptive_name](variables.tf#L57) | Name of the project name. Used for project name instead of `name` variable. | <code>string</code> | | <code>null</code> |
| [dns_zones](variables.tf#L63) | DNS private zones to create as child of var.defaults.environment_dns_zone. | <code>list&#40;string&#41;</code> | | <code>&#91;&#93;</code> |
| [essential_contacts](variables.tf#L69) | Email contacts to be used for billing and GCP notifications. | <code>list&#40;string&#41;</code> | | <code>&#91;&#93;</code> |
| [folder_id](variables.tf#L75) | Folder ID for the folder where the project will be created. | <code>string</code> | | <code>null</code> |
| [group_iam](variables.tf#L81) | Custom IAM settings in group => [role] format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [group_iam_additive](variables.tf#L87) | Custom additive IAM settings in group => [role] format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam](variables.tf#L93) | Custom IAM settings in role => [principal] format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_additive](variables.tf#L99) | Custom additive IAM settings in role => [principal] format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [kms_service_agents](variables.tf#L105) | KMS IAM configuration in as service => [key]. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [labels](variables.tf#L111) | Labels to be assigned at project level. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [org_policies](variables.tf#L117) | Org-policy overrides at project level. | <code title="map&#40;object&#40;&#123;&#10; inherit_from_parent &#61; optional&#40;bool&#41; &#35; for list policies only.&#10; reset &#61; optional&#40;bool&#41;&#10; rules &#61; optional&#40;list&#40;object&#40;&#123;&#10; allow &#61; optional&#40;object&#40;&#123;&#10; all &#61; optional&#40;bool&#41;&#10; values &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;&#10; deny &#61; optional&#40;object&#40;&#123;&#10; all &#61; optional&#40;bool&#41;&#10; values &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;&#10; enforce &#61; optional&#40;bool&#41; &#35; for boolean policies only.&#10; condition &#61; optional&#40;object&#40;&#123;&#10; description &#61; optional&#40;string&#41;&#10; expression &#61; optional&#40;string&#41;&#10; location &#61; optional&#40;string&#41;&#10; title &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [service_accounts](variables.tf#L158) | Service accounts to be created, and roles assigned them on the project. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [service_accounts_additive](variables.tf#L164) | Service accounts to be created, and roles assigned them on the project additively. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [service_accounts_iam](variables.tf#L170) | IAM bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}. | <code>map&#40;map&#40;list&#40;string&#41;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [service_accounts_iam_additive](variables.tf#L177) | IAM additive bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}. | <code>map&#40;map&#40;list&#40;string&#41;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [service_identities_iam](variables.tf#L184) | Custom IAM settings for service identities in service => [role] format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [service_identities_iam_additive](variables.tf#L191) | Custom additive IAM settings for service identities in service => [role] format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [services](variables.tf#L198) | Services to be enabled for the project. | <code>list&#40;string&#41;</code> | | <code>&#91;&#93;</code> |
| [vpc](variables.tf#L205) | VPC configuration for the project. | <code title="object&#40;&#123;&#10; host_project &#61; string&#10; gke_setup &#61; optional&#40;object&#40;&#123;&#10; enable_security_admin &#61; optional&#40;bool, false&#41;&#10; enable_host_service_agent &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; service_iam_grants &#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; subnets_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; host_project &#61; null&#10;&#125;">&#123;&#8230;&#125;</code> |
| [factory_data](variables.tf#L83) | Project data from either YAML files or externally parsed data. | <code title="object&#40;&#123;&#10; data &#61; optional&#40;map&#40;any&#41;&#41;&#10; data_path &#61; optional&#40;string&#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; 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; service_identity_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; &#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; default_roles &#61; optional&#40;bool, true&#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#L44) | 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; default_roles &#61; optional&#40;bool, true&#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#L63) | 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; 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; default_roles &#61; optional&#40;bool, true&#41;&#10; &#125;&#41;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [project](outputs.tf#L19) | The project resource as return by the `project` module. | |
| [project_id](outputs.tf#L29) | Project ID. | |
| [projects](outputs.tf#L17) | Project module outputs. | |
| [service_accounts](outputs.tf#L22) | Service account emails. | |
<!-- END TFDOC -->

View File

@ -0,0 +1,100 @@
/**
* Copyright 2023 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 {
_data = (
var.factory_data.data != null
? var.factory_data.data
: {
for f in fileset("${local._data_path}", "**/*.yaml") :
trimsuffix(f, ".yaml") => yamldecode(file("${local._data_path}/${f}"))
}
)
_data_path = var.factory_data.data_path == null ? null : pathexpand(
var.factory_data.data_path
)
projects = {
for k, v in local._data : k => merge(v, {
billing_account = coalesce(
var.data_overrides.billing_account,
try(v.billing_account, null),
var.data_defaults.billing_account
)
contacts = coalesce(
var.data_overrides.contacts,
try(v.contacts, null),
var.data_defaults.contacts
)
labels = coalesce(
try(v.labels, null),
var.data_defaults.labels
)
metric_scopes = coalesce(
try(v.metric_scopes, null),
var.data_defaults.metric_scopes
)
prefix = coalesce(
var.data_overrides.prefix,
try(v.prefix, null),
var.data_defaults.prefix
)
service_encryption_key_ids = coalesce(
var.data_overrides.service_encryption_key_ids,
try(v.service_encryption_key_ids, null),
var.data_defaults.service_encryption_key_ids
)
service_perimeter_bridges = coalesce(
var.data_overrides.service_perimeter_bridges,
try(v.service_perimeter_bridges, null),
var.data_defaults.service_perimeter_bridges
)
service_perimeter_standard = try(coalesce(
var.data_overrides.service_perimeter_standard,
try(v.service_perimeter_standard, null),
var.data_defaults.service_perimeter_standard
), null)
services = coalesce(
var.data_overrides.services,
try(v.services, null),
var.data_defaults.services
)
shared_vpc_service_config = coalesce(
try(v.shared_vpc_service_config, null),
var.data_defaults.shared_vpc_service_config
)
tag_bindings = coalesce(
var.data_overrides.tag_bindings,
try(v.tag_bindings, null),
var.data_defaults.tag_bindings
)
# non-project resources
service_accounts = coalesce(
var.data_overrides.service_accounts,
try(v.service_accounts, null),
var.data_defaults.service_accounts
)
})
}
service_accounts = flatten([
for k, v in local.projects : [
for name, opts in v.service_accounts : {
project = k
name = name
options = opts
}
]
])
}

View File

@ -14,222 +14,68 @@
* limitations under the License.
*/
locals {
_gke_config_service_identity_iam = {
"roles/compute.networkUser" = compact([
var.vpc.gke_setup.enable_host_service_agent ? "container-engine" : null,
local.vpc_cloudservices ? "cloudservices" : null
])
"roles/compute.securityAdmin" = compact([
var.vpc.gke_setup.enable_security_admin ? "container-engine" : null,
])
"roles/container.hostServiceAgentUser" = compact([
var.vpc.gke_setup.enable_host_service_agent ? "container-engine" : null
])
}
_group_iam = {
for r in local._group_iam_bindings : r => [
for k, v in var.group_iam :
"group:${k}" if try(index(v, r), null) != null
]
}
_group_iam_additive = {
for r in local._group_iam_additive_bindings : r => [
for k, v in var.group_iam_additive :
"group:${k}" if try(index(v, r), null) != null
]
}
_group_iam_bindings = distinct(flatten(values(var.group_iam)))
_group_iam_additive_bindings = distinct(flatten(values(var.group_iam_additive)))
_service_accounts_iam = {
for r in local._service_accounts_iam_bindings : r => [
for k, v in var.service_accounts :
module.service-accounts[k].iam_email
if try(index(v, r), null) != null
]
}
_service_accounts_iam_bindings = distinct(flatten(
values(var.service_accounts)
module "projects" {
source = "../../../modules/project"
for_each = local.projects
billing_account = each.value.billing_account
name = each.key
parent = try(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, {})
# TODO: concat lists for each key
contacts = merge(
each.value.contacts, var.data_merges.contacts
)
default_service_account = try(each.value.default_service_account, "keep")
descriptive_name = try(each.value.descriptive_name, null)
group_iam = try(each.value.group_iam, {})
iam = try(each.value.iam, {})
iam_bindings = try(each.value.iam_bindings, {})
iam_bindings_additive = try(each.value.iam_bindings_additive, {})
labels = each.value.labels
lien_reason = try(each.value.lien_reason, null)
logging_data_access = try(each.value.logging_data_access, {})
logging_exclusions = try(each.value.logging_exclusions, {})
logging_sinks = try(each.value.logging_sinks, {})
metric_scopes = distinct(concat(
each.value.metric_scopes, var.data_merges.metric_scopes
))
_service_accounts_iam_additive = {
for r in local._service_accounts_iam_additive_bindings : r => [
for k, v in var.service_accounts_additive :
module.service-accounts[k].iam_email
if try(index(v, r), null) != null
]
}
_service_accounts_iam_additive_bindings = distinct(flatten(
values(var.service_accounts_additive)
service_encryption_key_ids = merge(
each.value.service_encryption_key_ids,
var.data_merges.service_encryption_key_ids
)
service_perimeter_bridges = distinct(concat(
each.value.service_perimeter_bridges,
var.data_merges.service_perimeter_bridges
))
_services = concat([
"billingbudgets.googleapis.com",
"essentialcontacts.googleapis.com",
"orgpolicy.googleapis.com",
],
length(var.dns_zones) > 0 ? ["dns.googleapis.com"] : [],
try(var.vpc.gke_setup, null) != null ? ["container.googleapis.com"] : [],
var.vpc != null ? ["compute.googleapis.com"] : [],
service_perimeter_standard = each.value.service_perimeter_standard
services = distinct(concat(
each.value.services,
var.data_merges.services
))
shared_vpc_service_config = each.value.shared_vpc_service_config
tag_bindings = merge(
each.value.tag_bindings,
var.data_merges.tag_bindings
)
_service_identities_roles = distinct(flatten(values(var.service_identities_iam)))
_service_identities_iam = {
for role in local._service_identities_roles : role => [
for service, roles in var.service_identities_iam :
"serviceAccount:${module.project.service_accounts.robots[service]}"
if contains(roles, role)
]
}
_service_identities_roles_additive = distinct(flatten(values(var.service_identities_iam_additive)))
_service_identities_iam_additive = {
for role in local._service_identities_roles_additive : role => [
for service, roles in var.service_identities_iam_additive :
"serviceAccount:${module.project.service_accounts.robots[service]}"
if contains(roles, role)
]
}
_vpc_subnet_bindings = (
var.vpc.subnets_iam == null || var.vpc.host_project == null
? []
: flatten([
for subnet, members in var.vpc.subnets_iam : [
for member in members : {
region = split("/", subnet)[0]
subnet = split("/", subnet)[1]
member = member
}
]
])
)
billing_account_id = coalesce(
var.billing_account_id, try(var.defaults.billing_account_id, "")
)
billing_alert = (
var.billing_alert == null
? try(var.defaults.billing_alert, null)
: var.billing_alert
)
essential_contacts = concat(
try(var.defaults.essential_contacts, []), var.essential_contacts
)
iam = {
for role in distinct(concat(
keys(var.iam),
keys(local._group_iam),
keys(local._service_accounts_iam),
keys(local._service_identities_iam),
)) :
role => concat(
try(var.iam[role], []),
try(local._group_iam[role], []),
try(local._service_accounts_iam[role], []),
try(local._service_identities_iam[role], []),
)
}
iam_additive = {
for role in distinct(concat(
keys(var.iam_additive),
keys(local._group_iam_additive),
keys(local._service_accounts_iam_additive),
keys(local._service_identities_iam_additive),
)) :
role => concat(
try(var.iam_additive[role], []),
try(local._group_iam_additive[role], []),
try(local._service_accounts_iam_additive[role], []),
try(local._service_identities_iam_additive[role], []),
)
}
labels = merge(
coalesce(var.labels, {}), coalesce(try(var.defaults.labels, {}), {})
)
services = distinct(concat(var.services, local._services))
vpc_cloudservices = (
var.vpc.gke_setup.enable_host_service_agent ||
contains(var.services, "compute.googleapis.com")
)
vpc_service_identity_iam = {
for role in setunion(keys(local._gke_config_service_identity_iam), keys(var.vpc.service_identity_iam)) :
role => setunion(
lookup(local._gke_config_service_identity_iam, role, []),
lookup(var.vpc.service_identity_iam, role, []),
)
}
vpc_subnet_bindings = {
for binding in local._vpc_subnet_bindings :
"${binding.subnet}:${binding.member}" => binding
}
}
module "billing-alert" {
for_each = local.billing_alert == null ? {} : { 1 = 1 }
source = "../../../modules/billing-budget"
billing_account = local.billing_account_id
name = "${module.project.project_id} budget"
amount = local.billing_alert.amount
thresholds = local.billing_alert.thresholds
credit_treatment = local.billing_alert.credit_treatment
notification_channels = var.defaults.notification_channels
projects = ["projects/${module.project.number}"]
email_recipients = {
project_id = module.project.project_id
emails = local.essential_contacts
}
}
module "dns" {
source = "../../../modules/dns"
for_each = toset(var.dns_zones)
project_id = coalesce(var.vpc.host_project, module.project.project_id)
name = each.value
zone_config = {
domain = "${each.value}.${var.defaults.environment_dns_zone}"
private = {
client_networks = [var.defaults.shared_vpc_self_link]
}
}
}
module "project" {
source = "../../../modules/project"
billing_account = local.billing_account_id
name = var.project_id
descriptive_name = var.descriptive_name
prefix = var.prefix
contacts = { for c in local.essential_contacts : c => ["ALL"] }
iam = local.iam
iam_additive = local.iam_additive
labels = local.labels
org_policies = try(var.org_policies, {})
parent = var.folder_id
service_encryption_key_ids = var.kms_service_agents
services = local.services
shared_vpc_service_config = var.vpc == null ? null : {
host_project = var.vpc.host_project
# these are non-authoritative
service_identity_iam = local.vpc_service_identity_iam
service_iam_grants = var.vpc.service_iam_grants
}
}
module "service-accounts" {
source = "../../../modules/iam-service-account"
for_each = var.service_accounts
name = each.key
project_id = module.project.project_id
iam = lookup(var.service_accounts_iam, each.key, null)
}
resource "google_compute_subnetwork_iam_member" "default" {
for_each = local.vpc_subnet_bindings
project = var.vpc.host_project
subnetwork = "projects/${var.vpc.host_project}/regions/${each.value.region}/subnetworks/${each.value.subnet}"
region = each.value.region
role = "roles/compute.networkUser"
member = (
lookup(var.service_accounts, each.value.member, null) != null
? module.service-accounts[each.value.member].iam_email
: each.value.member
source = "../../../modules/iam-service-account"
for_each = {
for k in local.service_accounts : "${k.project}-${k.name}" => k
}
name = each.value.name
project_id = module.projects[each.value.project].project_id
iam_project_roles = (
try(each.value.options.default_roles, null) == null
? {}
: {
(module.projects[each.value.project].project_id) = [
"roles/logging.logWriter",
"roles/monitoring.metricWriter"
]
}
)
}

View File

@ -1,5 +1,5 @@
/**
* Copyright 2022 Google LLC
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,23 +14,15 @@
* limitations under the License.
*/
# TODO(): proper outputs
output "project" {
description = "The project resource as return by the `project` module."
value = module.project
depends_on = [
google_compute_subnetwork_iam_member.default,
module.dns
]
output "projects" {
description = "Project module outputs."
value = module.projects
}
output "project_id" {
description = "Project ID."
value = module.project.project_id
depends_on = [
google_compute_subnetwork_iam_member.default,
module.dns
]
output "service_accounts" {
description = "Service account emails."
# TODO: group by project
value = {
for k, v in module.service-accounts : k => v.email
}
}

View File

@ -1,29 +0,0 @@
# skip boilerplate check
billing_account_id: 012345-67890A-BCDEF0
# [opt] Setup for billing alerts
billing_alert:
amount: 1000
thresholds:
current: [0.5, 0.8]
forecasted: [0.5, 0.8]
credit_treatment: INCLUDE_ALL_CREDITS
environment_dns_zone: dev.example.org
# [opt] Contacts for billing alerts and important notifications
essential_contacts: ["team-contacts@example.com"]
# [opt] Labels set for all projects
labels:
environment: dev
department: accounting
application: example-app
foo: bar
# [opt] Additional notification channels for billing
notification_channels: []
shared_vpc_self_link: projects/foo/networks/bar
prefix: test
vpc_host_project:

View File

@ -1,117 +0,0 @@
# skip boilerplate check
# [opt] Billing account id - overrides default if set
billing_account_id: 012345-67890A-BCDEF0
# [opt] Billing alerts config - overrides default if set
billing_alert:
amount: 10
thresholds:
current:
- 0.5
- 0.8
forecasted: []
credit_treatment: INCLUDE_ALL_CREDITS
# [opt] DNS zones to be created as children of the environment_dns_zone defined in defaults
dns_zones:
- lorem
- ipsum
# [opt] Contacts for billing alerts and important notifications
essential_contacts:
- team-a-contacts@example.com
# Folder the project will be created as children of
folder_id: folders/012345678901
# [opt] Authoritative IAM bindings in group => [roles] format
group_iam:
test-team-foobar@fast-lab-0.gcp-pso-italy.net:
- roles/compute.admin
# [opt] Authoritative IAM bindings in role => [principals] format
# Generally used to grant roles to service accounts external to the project
iam:
roles/compute.admin:
- serviceAccount:service-account
# [opt] Service robots and keys they will be assigned as cryptoKeyEncrypterDecrypter
# in service => [keys] format
kms_service_agents:
compute: [key1, key2]
storage: [key1, key2]
# [opt] Labels for the project - merged with the ones defined in defaults
labels:
environment: dev2
costcenter: apps
# [opt] Org policy overrides defined at project level
org_policies:
compute.disableGuestAttributesAccess:
rules:
- enforce: true
compute.trustedImageProjects:
rules:
- allow:
values:
- projects/fast-dev-iac-core-0
compute.vmExternalIpAccess:
rules:
- deny:
all: true
# [opt] Prefix - overrides default if set
prefix: test1
# [opt] Service account to create for the project and their roles on the project
# in name => [roles] format
service_accounts:
another-service-account:
- roles/compute.admin
my-service-account:
- roles/compute.adminv1
# [opt] APIs to enable on the project.
services:
- storage.googleapis.com
- stackdriver.googleapis.com
- compute.googleapis.com
# [opt] Roles to assign to the service identities in service => [roles] format
service_identities_iam:
compute:
- roles/storage.objectViewer
# [opt] VPC setup.
# If set enables the `compute.googleapis.com` service and configures
# service project attachment
vpc:
# [opt] If set, enables the container API
gke_setup:
# Grants "roles/container.hostServiceAgentUser" to the container robot if set
enable_host_service_agent: false
# Grants "roles/compute.securityAdmin" to the container robot if set
enable_security_admin: true
# Host project the project will be service project of
host_project: fast-dev-net-spoke-0
# [opt] Services for which set up the IAM in the host project
service_iam_grants:
- dataproc.googleapis.com
# [opt] Roles to rant service project service identities in host project
service_identity_iam:
"roles/compute.networkUser":
- cloudservices
- container-engine
# [opt] Subnets in the host project where principals will be granted networkUser
# in region/subnet-name => [principals]
subnets_iam:
europe-west1/dev-default-ew1:
- user:foobar@example.com
- serviceAccount:my-service-account

View File

@ -14,215 +14,84 @@
* limitations under the License.
*/
variable "billing_account_id" {
description = "Billing account id."
type = string
}
variable "billing_alert" {
description = "Billing alert configuration."
variable "data_defaults" {
description = "Optional default values used when corresponding project data from files are missing."
type = object({
amount = number
thresholds = object({
current = list(number)
forecasted = list(number)
})
credit_treatment = string
billing_account = optional(string)
contacts = optional(map(list(string)), {})
labels = optional(map(string), {})
metric_scopes = optional(list(string), [])
prefix = optional(string)
service_encryption_key_ids = optional(map(list(string)), {})
service_perimeter_bridges = optional(list(string), [])
service_perimeter_standard = optional(string)
services = optional(list(string), [])
shared_vpc_service_config = optional(object({
host_project = string
service_identity_iam = optional(map(list(string)), {})
service_iam_grants = optional(list(string), [])
}), { host_project = null })
tag_bindings = optional(map(string), {})
# non-project resources
service_accounts = optional(map(object({
default_roles = optional(bool, true)
})), {})
})
default = null
}
variable "defaults" {
description = "Project factory default values."
type = object({
billing_account_id = string
billing_alert = object({
amount = number
thresholds = object({
current = list(number)
forecasted = list(number)
})
credit_treatment = string
})
environment_dns_zone = string
essential_contacts = list(string)
labels = map(string)
notification_channels = list(string)
shared_vpc_self_link = string
vpc_host_project = string
})
default = null
}
variable "descriptive_name" {
description = "Name of the project name. Used for project name instead of `name` variable."
type = string
default = null
}
variable "dns_zones" {
description = "DNS private zones to create as child of var.defaults.environment_dns_zone."
type = list(string)
default = []
}
variable "essential_contacts" {
description = "Email contacts to be used for billing and GCP notifications."
type = list(string)
default = []
}
variable "folder_id" {
description = "Folder ID for the folder where the project will be created."
type = string
default = null
}
variable "group_iam" {
description = "Custom IAM settings in group => [role] format."
type = map(list(string))
default = {}
}
variable "group_iam_additive" {
description = "Custom additive IAM settings in group => [role] format."
type = map(list(string))
default = {}
}
variable "iam" {
description = "Custom IAM settings in role => [principal] format."
type = map(list(string))
default = {}
}
variable "iam_additive" {
description = "Custom additive IAM settings in role => [principal] format."
type = map(list(string))
default = {}
}
variable "kms_service_agents" {
description = "KMS IAM configuration in as service => [key]."
type = map(list(string))
default = {}
}
variable "labels" {
description = "Labels to be assigned at project level."
type = map(string)
default = {}
}
variable "org_policies" {
description = "Org-policy overrides at project level."
type = map(object({
inherit_from_parent = optional(bool) # for list policies only.
reset = optional(bool)
rules = optional(list(object({
allow = optional(object({
all = optional(bool)
values = optional(list(string))
}))
deny = optional(object({
all = optional(bool)
values = optional(list(string))
}))
enforce = optional(bool) # for boolean policies only.
condition = optional(object({
description = optional(string)
expression = optional(string)
location = optional(string)
title = optional(string)
}), {})
})), [])
}))
nullable = false
default = {}
nullable = false
}
variable "prefix" {
description = "Prefix used for resource names."
type = string
validation {
condition = var.prefix != ""
error_message = "Prefix cannot be empty."
}
}
variable "project_id" {
description = "Project id."
type = string
}
variable "service_accounts" {
description = "Service accounts to be created, and roles assigned them on the project."
type = map(list(string))
default = {}
}
variable "service_accounts_additive" {
description = "Service accounts to be created, and roles assigned them on the project additively."
type = map(list(string))
default = {}
}
variable "service_accounts_iam" {
description = "IAM bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}."
type = map(map(list(string)))
default = {}
nullable = false
}
variable "service_accounts_iam_additive" {
description = "IAM additive bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}."
type = map(map(list(string)))
default = {}
nullable = false
}
variable "service_identities_iam" {
description = "Custom IAM settings for service identities in service => [role] format."
type = map(list(string))
default = {}
nullable = false
}
variable "service_identities_iam_additive" {
description = "Custom additive IAM settings for service identities in service => [role] format."
type = map(list(string))
default = {}
nullable = false
}
variable "services" {
description = "Services to be enabled for the project."
type = list(string)
default = []
nullable = false
}
variable "vpc" {
description = "VPC configuration for the project."
variable "data_merges" {
description = "Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`."
type = object({
host_project = string
gke_setup = optional(object({
enable_security_admin = optional(bool, false)
enable_host_service_agent = optional(bool, false)
}), {})
service_iam_grants = optional(list(string), [])
service_identity_iam = optional(map(list(string)), {})
subnets_iam = optional(map(list(string)), {})
contacts = optional(map(list(string)), {})
labels = optional(map(string), {})
metric_scopes = optional(list(string), [])
service_encryption_key_ids = optional(map(list(string)), {})
service_perimeter_bridges = optional(list(string), [])
services = optional(list(string), [])
tag_bindings = optional(map(string), {})
# non-project resources
service_accounts = optional(map(object({
default_roles = optional(bool, true)
})), {})
})
nullable = false
default = {}
}
variable "data_overrides" {
description = "Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`."
type = object({
billing_account = optional(string)
contacts = optional(map(list(string)))
prefix = optional(string)
service_encryption_key_ids = optional(map(list(string)))
service_perimeter_bridges = optional(list(string))
service_perimeter_standard = optional(string)
tag_bindings = optional(map(string))
services = optional(list(string))
# non-project resources
service_accounts = optional(map(object({
default_roles = optional(bool, true)
})))
})
nullable = false
default = {}
}
variable "factory_data" {
description = "Project data from either YAML files or externally parsed data."
type = object({
data = optional(map(any))
data_path = optional(string)
})
default = {
host_project = null
}
nullable = false
validation {
condition = var.vpc.host_project != null || (
var.vpc.host_project == null && length(var.vpc.gke_setup) == 0 && length(var.vpc.service_iam_grants) == 0 &&
length(var.vpc.service_identity_iam) == 0 && length(var.vpc.subnets_iam) == 0
)
error_message = "host_project is required if providing any additional configuration for vpc"
condition = (
(var.factory_data.data != null ? 1 : 0) +
(var.factory_data.data_path != null ? 1 : 0)
) == 1
error_message = "One of data or data_path needs to be set."
}
}

View File

@ -32,9 +32,11 @@ module "project" {
source = "../../../modules/project"
project_create = var.project_create != null
billing_account = try(var.project_create.billing_account, null)
oslogin = try(var.project_create.oslogin, false)
parent = try(var.project_create.parent, null)
name = var.project_id
compute_metadata = var.project_create.oslogin != true ? {} : {
enable-oslogin = "true"
}
parent = try(var.project_create.parent, null)
name = var.project_id
services = [
"compute.googleapis.com",
"container.googleapis.com"

View File

@ -41,8 +41,9 @@ module "project-svc-gce" {
prefix = var.prefix
name = "gce"
services = var.project_services
oslogin = true
oslogin_admins = var.owners_gce
compute_metadata = {
enable-oslogin = "true"
}
shared_vpc_service_config = {
host_project = module.project-host.project_id
service_identity_iam = {
@ -50,7 +51,8 @@ module "project-svc-gce" {
}
}
iam = {
"roles/owner" = var.owners_gce
"roles/compute.osAdminLogin" = var.owners_gce
"roles/owner" = var.owners_gce
}
}

View File

@ -10,7 +10,13 @@ This architecture can be used for the following use cases and more:
* Intranet / internal Wiki
* E-commerce platform
# Architecture
## TODO
* [ ] refactor variables merging WP configuration in a single variable
* [ ] optional creation of a remote artifact registry repository
* [ ] optional serverless connector
## Architecture
![Wordpress on Cloud Run](images/architecture.png "Wordpress on Cloud Run")
@ -20,9 +26,7 @@ The main components that are deployed in this architecture are the following (yo
* [Cloud SQL](https://cloud.google.com/sql): Managed solution for SQL databases
* [VPC Serverless Connector](https://cloud.google.com/vpc/docs/serverless-vpc-access): Solution to access the CloudSQL VPC from Cloud Run, using only internal IP addresses
# Setup
## Prerequisites
## Setup and deployment
### Setting up the project for the deployment
@ -30,8 +34,6 @@ This example will deploy all its resources into the project defined by the `proj
If `project_create` is left to null, the identity performing the deployment needs the `owner` role on the project defined by the `project_id` variable. Otherwise, the identity performing the deployment needs `resourcemanager.projectCreator` on the resource hierarchy node specified by `project_create.parent` and `billing.user` on the billing account specified by `project_create.billing_account_id`.
## Deployment
### Step 0: Cloning the repository
If you want to deploy from your Cloud Shell, click on the image below, sign in if required and when the prompt appears, click on “confirm”.
@ -116,20 +118,19 @@ terraform destroy
The above command will delete the associated resources so there will be no billable charges made afterwards.
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [prefix](variables.tf#L57) | Prefix used for resource names. | <code>string</code> | ✓ | |
| [prefix](variables.tf#L63) | Prefix used for resource names. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L81) | Project id, references existing project if `project_create` is null. | <code>string</code> | ✓ | |
| [wordpress_image](variables.tf#L92) | Image to run with Cloud Run, starts with \"gcr.io\". | <code>string</code> | ✓ | |
| [cloud_run_invoker](variables.tf#L18) | IAM member authorized to access the end-point (for example, 'user:YOUR_IAM_USER' for only you or 'allUsers' for everyone). | <code>string</code> | | <code>&#34;allUsers&#34;</code> |
| [cloudsql_password](variables.tf#L24) | CloudSQL password (will be randomly generated by default). | <code>string</code> | | <code>null</code> |
| [connector](variables.tf#L30) | Existing VPC serverless connector to use if not creating a new one. | <code>string</code> | | <code>null</code> |
| [create_connector](variables.tf#L36) | Should a VPC serverless connector be created or not. | <code>bool</code> | | <code>true</code> |
| [ip_ranges](variables.tf#L43) | CIDR blocks: VPC serverless connector, Private Service Access(PSA) for CloudSQL, CloudSQL VPC. | <code title="object&#40;&#123;&#10; connector &#61; string&#10; psa &#61; string&#10; sql_vpc &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; connector &#61; &#34;10.8.0.0&#47;28&#34;&#10; psa &#61; &#34;10.60.0.0&#47;24&#34;&#10; sql_vpc &#61; &#34;10.0.0.0&#47;20&#34;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [principals](variables.tf#L66) | List of users to give rights to (CloudSQL admin, client and instanceUser, Logging admin, Service Account User and TokenCreator), eg 'user@domain.com'. | <code>list&#40;string&#41;</code> | | <code>&#91;&#93;</code> |
| [admin_principal](variables.tf#L17) | User or group that is assigned roles, in IAM format (`group:foo@example.com`). | <code>string</code> | | <code>null</code> |
| [cloud_run_invoker](variables.tf#L24) | IAM member authorized to access the end-point (for example, 'user:YOUR_IAM_USER' for only you or 'allUsers' for everyone). | <code>string</code> | | <code>&#34;allUsers&#34;</code> |
| [cloudsql_password](variables.tf#L30) | CloudSQL password (will be randomly generated by default). | <code>string</code> | | <code>null</code> |
| [connector](variables.tf#L36) | Existing VPC serverless connector to use if not creating a new one. | <code>string</code> | | <code>null</code> |
| [create_connector](variables.tf#L42) | Should a VPC serverless connector be created or not. | <code>bool</code> | | <code>true</code> |
| [ip_ranges](variables.tf#L49) | CIDR blocks: VPC serverless connector, Private Service Access(PSA) for CloudSQL, CloudSQL VPC. | <code title="object&#40;&#123;&#10; connector &#61; string&#10; psa &#61; string&#10; sql_vpc &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; connector &#61; &#34;10.8.0.0&#47;28&#34;&#10; psa &#61; &#34;10.60.0.0&#47;24&#34;&#10; sql_vpc &#61; &#34;10.0.0.0&#47;20&#34;&#10;&#125;">&#123;&#8230;&#125;</code> |
| [project_create](variables.tf#L72) | Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | <code title="object&#40;&#123;&#10; billing_account_id &#61; string&#10; parent &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [region](variables.tf#L86) | Region for the created resources. | <code>string</code> | | <code>&#34;europe-west4&#34;</code> |
| [wordpress_password](variables.tf#L97) | Password for the Wordpress user (will be randomly generated by default). | <code>string</code> | | <code>null</code> |
@ -143,5 +144,20 @@ The above command will delete the associated resources so there will be no billa
| [cloudsql_password](outputs.tf#L23) | CloudSQL password. | ✓ |
| [wp_password](outputs.tf#L29) | Wordpress user password. | ✓ |
| [wp_user](outputs.tf#L35) | Wordpress username. | |
<!-- END TFDOC -->
## Test
```hcl
module "test" {
source = "./fabric/blueprints/third-party-solutions/wordpress/cloudrun"
admin_principal = "group:foo@example.com"
prefix = "wp-cr-test"
project_create = {
billing_account_id = "1234-ABCD-1234"
parent = "folders/1234563"
}
project_id = "test-prj"
wordpress_image = "gcr.io/myprj/wordpress"
}
# tftest modules=5 resources=33
```

View File

@ -16,30 +16,29 @@
locals {
all_principals_iam = [for k in var.principals : "user:${k}"]
cloudsql_conf = {
database_version = "MYSQL_8_0"
tier = "db-g1-small"
db = "wp-mysql"
user = "admin"
}
iam = {
# CloudSQL
"roles/cloudsql.admin" = local.all_principals_iam
"roles/cloudsql.client" = local.all_principals_iam
"roles/cloudsql.instanceUser" = local.all_principals_iam
# common roles
"roles/logging.admin" = local.all_principals_iam
"roles/iam.serviceAccountUser" = local.all_principals_iam
"roles/iam.serviceAccountTokenCreator" = local.all_principals_iam
}
iam_roles = [
"roles/cloudsql.admin",
"roles/cloudsql.client",
"roles/cloudsql.instanceUser",
"roles/logging.admin",
"roles/iam.serviceAccountUser",
"roles/iam.serviceAccountTokenCreator"
]
connector = var.connector == null ? google_vpc_access_connector.connector.0.self_link : var.connector
wp_user = "user"
wp_pass = var.wordpress_password == null ? random_password.wp_password.result : var.wordpress_password
}
resource "random_password" "wp_password" {
length = 8
}
# either create a project or set up the given one
module "project" {
source = "../../../../modules/project"
name = var.project_id
@ -47,8 +46,19 @@ module "project" {
billing_account = try(var.project_create.billing_account_id, null)
project_create = var.project_create != null
prefix = var.project_create == null ? null : var.prefix
iam = var.project_create != null ? local.iam : {}
iam_additive = var.project_create == null ? local.iam : {}
iam = (
var.project_create != true || var.admin_principal == null
? {}
: { for r in local.iam_roles : r => [var.admin_principal] }
)
iam_bindings_additive = (
var.project_create == true || var.admin_principal == null
? {}
: { for r in local.iam_roles : r => {
member = var.admin_principal
role = r
} }
)
services = [
"run.googleapis.com",
"logging.googleapis.com",
@ -60,60 +70,45 @@ module "project" {
]
}
resource "random_password" "wp_password" {
length = 8
}
# create the Cloud Run service
module "cloud_run" {
source = "../../../../modules/cloud-run"
project_id = module.project.project_id
name = "${var.prefix}-cr-wordpress"
region = var.region
containers = [{
image = var.wordpress_image
ports = [{
name = "http1"
protocol = null
container_port = var.wordpress_port
}]
options = {
command = null
args = null
env_from = null
# set up the database connection
containers = {
wp = {
image = var.wordpress_image
ports = {
http = { container_port = var.wordpress_port }
}
env = {
"APACHE_HTTP_PORT_NUMBER" : var.wordpress_port
"WORDPRESS_DATABASE_HOST" : module.cloudsql.ip
"WORDPRESS_DATABASE_NAME" : local.cloudsql_conf.db
"WORDPRESS_DATABASE_USER" : local.cloudsql_conf.user
"WORDPRESS_DATABASE_PASSWORD" : var.cloudsql_password == null ? module.cloudsql.user_passwords[local.cloudsql_conf.user] : var.cloudsql_password
"WORDPRESS_USERNAME" : local.wp_user
"WORDPRESS_PASSWORD" : local.wp_pass
APACHE_HTTP_PORT_NUMBER = var.wordpress_port
WORDPRESS_DATABASE_HOST = module.cloudsql.ip
WORDPRESS_DATABASE_NAME = local.cloudsql_conf.db
WORDPRESS_DATABASE_USER = local.cloudsql_conf.user
WORDPRESS_DATABASE_PASSWORD = (
var.cloudsql_password == null
? module.cloudsql.user_passwords[local.cloudsql_conf.user]
: var.cloudsql_password
)
WORDPRESS_USERNAME = local.wp_user
WORDPRESS_PASSWORD = local.wp_pass
}
}
resources = null
volume_mounts = null
}]
}
iam = {
"roles/run.invoker" : [var.cloud_run_invoker]
}
revision_annotations = {
autoscaling = {
min_scale = 1
max_scale = 2
}
# connect to CloudSQL
cloudsql_instances = [module.cloudsql.connection_name]
vpcaccess_connector = null
cloudsql_instances = [module.cloudsql.connection_name]
# allow all traffic
vpcaccess_egress = "all-traffic"
vpcaccess_connector = local.connector
vpcaccess_egress = "all-traffic"
}
ingress_settings = "all"
}

View File

@ -14,6 +14,12 @@
* limitations under the License.
*/
variable "admin_principal" {
description = "User or group that is assigned roles, in IAM format (`group:foo@example.com`)."
type = string
default = null
}
# Documentation: https://cloud.google.com/run/docs/securing/managing-access#making_a_service_public
variable "cloud_run_invoker" {
type = string
@ -63,12 +69,6 @@ variable "prefix" {
}
}
variable "principals" {
description = "List of users to give rights to (CloudSQL admin, client and instanceUser, Logging admin, Service Account User and TokenCreator), eg 'user@domain.com'."
type = list(string)
default = []
}
variable "project_create" {
description = "Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format."
type = object({

View File

@ -186,7 +186,7 @@ This configuration is possible but unsupported and only exists for development p
| [identity-providers.tf](./identity-providers.tf) | Workload Identity Federation provider definitions. | | <code>google_iam_workload_identity_pool</code> · <code>google_iam_workload_identity_pool_provider</code> |
| [log-export.tf](./log-export.tf) | Audit log project and sink. | <code>bigquery-dataset</code> · <code>gcs</code> · <code>logging-bucket</code> · <code>project</code> · <code>pubsub</code> | |
| [main.tf](./main.tf) | Module-level locals and resources. | <code>folder</code> | |
| [organization.tf](./organization.tf) | Organization tag and conditional IAM grant. | <code>organization</code> | <code>google_organization_iam_member</code> · <code>google_tags_tag_value_iam_member</code> |
| [organization.tf](./organization.tf) | Organization tag and conditional IAM grant. | <code>organization</code> | <code>google_tags_tag_value_iam_member</code> |
| [outputs-files.tf](./outputs-files.tf) | Output files persistence to local filesystem. | | <code>local_file</code> |
| [outputs-gcs.tf](./outputs-gcs.tf) | Output files persistence to automation GCS bucket. | | <code>google_storage_bucket_object</code> |
| [outputs.tf](./outputs.tf) | Module outputs. | | |
@ -198,24 +198,25 @@ This configuration is possible but unsupported and only exists for development p
|---|---|:---:|:---:|:---:|:---:|
| [automation](variables.tf#L20) | Automation resources created by the organization-level bootstrap stage. | <code title="object&#40;&#123;&#10; outputs_bucket &#61; string&#10; project_id &#61; string&#10; project_number &#61; string&#10; federated_identity_pool &#61; string&#10; federated_identity_providers &#61; map&#40;object&#40;&#123;&#10; audiences &#61; list&#40;string&#41;&#10; issuer &#61; string&#10; issuer_uri &#61; string&#10; name &#61; string&#10; principal_tpl &#61; string&#10; principalset_tpl &#61; string&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | <code>0-bootstrap</code> |
| [billing_account](variables.tf#L39) | Billing account id. If billing account is not part of the same org set `is_org_level` to `false`. To disable handling of billing IAM roles set `no_iam` to `true`. | <code title="object&#40;&#123;&#10; id &#61; string&#10; is_org_level &#61; optional&#40;bool, true&#41;&#10; no_iam &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | |
| [organization](variables.tf#L192) | Organization details. | <code title="object&#40;&#123;&#10; domain &#61; string&#10; id &#61; number&#10; customer_id &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | <code>0-bootstrap</code> |
| [prefix](variables.tf#L208) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
| [tag_keys](variables.tf#L231) | Organization tag keys. | <code title="object&#40;&#123;&#10; context &#61; string&#10; environment &#61; string&#10; tenant &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | <code>1-resman</code> |
| [tag_names](variables.tf#L242) | Customized names for resource management tags. | <code title="object&#40;&#123;&#10; context &#61; string&#10; environment &#61; string&#10; tenant &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | <code>1-resman</code> |
| [tag_values](variables.tf#L253) | Organization resource management tag values. | <code>map&#40;string&#41;</code> | ✓ | | <code>1-resman</code> |
| [tenant_config](variables.tf#L260) | Tenant configuration. Short name must be 4 characters or less. If `short_name_is_prefix` is true, short name must be 9 characters or less, and will be used as the prefix as is. | <code title="object&#40;&#123;&#10; descriptive_name &#61; string&#10; groups &#61; object&#40;&#123;&#10; gcp-admins &#61; string&#10; gcp-devops &#61; optional&#40;string&#41;&#10; gcp-network-admins &#61; optional&#40;string&#41;&#10; gcp-security-admins &#61; optional&#40;string&#41;&#10; &#125;&#41;&#10; short_name &#61; string&#10; short_name_is_prefix &#61; optional&#40;bool, false&#41;&#10; fast_features &#61; optional&#40;object&#40;&#123;&#10; data_platform &#61; optional&#40;bool&#41;&#10; gke &#61; optional&#40;bool&#41;&#10; project_factory &#61; optional&#40;bool&#41;&#10; sandbox &#61; optional&#40;bool&#41;&#10; teams &#61; optional&#40;bool&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; locations &#61; optional&#40;object&#40;&#123;&#10; bq &#61; optional&#40;string&#41;&#10; gcs &#61; optional&#40;string&#41;&#10; logging &#61; optional&#40;string&#41;&#10; pubsub &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | |
| [organization](variables.tf#L214) | Organization details. | <code title="object&#40;&#123;&#10; domain &#61; string&#10; id &#61; number&#10; customer_id &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | <code>0-bootstrap</code> |
| [prefix](variables.tf#L230) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
| [tag_keys](variables.tf#L253) | Organization tag keys. | <code title="object&#40;&#123;&#10; context &#61; string&#10; environment &#61; string&#10; tenant &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | <code>1-resman</code> |
| [tag_names](variables.tf#L264) | Customized names for resource management tags. | <code title="object&#40;&#123;&#10; context &#61; string&#10; environment &#61; string&#10; tenant &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | <code>1-resman</code> |
| [tag_values](variables.tf#L275) | Organization resource management tag values. | <code>map&#40;string&#41;</code> | ✓ | | <code>1-resman</code> |
| [tenant_config](variables.tf#L282) | Tenant configuration. Short name must be 4 characters or less. If `short_name_is_prefix` is true, short name must be 9 characters or less, and will be used as the prefix as is. | <code title="object&#40;&#123;&#10; descriptive_name &#61; string&#10; groups &#61; object&#40;&#123;&#10; gcp-admins &#61; string&#10; gcp-devops &#61; optional&#40;string&#41;&#10; gcp-network-admins &#61; optional&#40;string&#41;&#10; gcp-security-admins &#61; optional&#40;string&#41;&#10; &#125;&#41;&#10; short_name &#61; string&#10; short_name_is_prefix &#61; optional&#40;bool, false&#41;&#10; fast_features &#61; optional&#40;object&#40;&#123;&#10; data_platform &#61; optional&#40;bool&#41;&#10; gke &#61; optional&#40;bool&#41;&#10; project_factory &#61; optional&#40;bool&#41;&#10; sandbox &#61; optional&#40;bool&#41;&#10; teams &#61; optional&#40;bool&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; locations &#61; optional&#40;object&#40;&#123;&#10; bq &#61; optional&#40;string&#41;&#10; gcs &#61; optional&#40;string&#41;&#10; logging &#61; optional&#40;string&#41;&#10; pubsub &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | |
| [cicd_repositories](variables.tf#L49) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | <code title="object&#40;&#123;&#10; bootstrap &#61; optional&#40;object&#40;&#123;&#10; branch &#61; optional&#40;string&#41;&#10; identity_provider &#61; string&#10; name &#61; string&#10; type &#61; string&#10; &#125;&#41;&#41;&#10; resman &#61; optional&#40;object&#40;&#123;&#10; branch &#61; optional&#40;string&#41;&#10; identity_provider &#61; string&#10; name &#61; string&#10; type &#61; string&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> | |
| [custom_roles](variables.tf#L95) | Custom roles defined at the organization level, in key => id format. | <code title="object&#40;&#123;&#10; service_project_network_admin &#61; string&#10; tenant_network_admin &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> | <code>0-bootstrap</code> |
| [fast_features](variables.tf#L105) | Selective control for top-level FAST features. | <code title="object&#40;&#123;&#10; data_platform &#61; optional&#40;bool, true&#41;&#10; gke &#61; optional&#40;bool, true&#41;&#10; project_factory &#61; optional&#40;bool, true&#41;&#10; sandbox &#61; optional&#40;bool, true&#41;&#10; teams &#61; optional&#40;bool, true&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | <code>0-bootstrap</code> |
| [federated_identity_providers](variables.tf#L119) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | <code title="map&#40;object&#40;&#123;&#10; attribute_condition &#61; optional&#40;string&#41;&#10; issuer &#61; string&#10; custom_settings &#61; optional&#40;object&#40;&#123;&#10; issuer_uri &#61; optional&#40;string&#41;&#10; audiences &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [group_iam](variables.tf#L133) | Tenant-level custom group IAM settings in group => [roles] format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [iam](variables.tf#L139) | Tenant-level custom IAM settings in role => [principal] format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [iam_additive](variables.tf#L145) | Tenant-level custom IAM settings in role => [principal] format for non-authoritative bindings. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [locations](variables.tf#L151) | Optional locations for GCS, BigQuery, and logging buckets created here. These are the defaults set at the organization level, and can be overridden via the tenant config variable. | <code title="object&#40;&#123;&#10; bq &#61; string&#10; gcs &#61; string&#10; logging &#61; string&#10; pubsub &#61; list&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; bq &#61; &#34;EU&#34;&#10; gcs &#61; &#34;EU&#34;&#10; logging &#61; &#34;global&#34;&#10; pubsub &#61; &#91;&#93;&#10;&#125;">&#123;&#8230;&#125;</code> | <code>0-bootstrap</code> |
| [log_sinks](variables.tf#L171) | Tenant-level log sinks, in name => {type, filter} format. | <code title="map&#40;object&#40;&#123;&#10; filter &#61; string&#10; type &#61; string&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code title="&#123;&#10; audit-logs &#61; &#123;&#10; filter &#61; &#34;logName:&#92;&#34;&#47;logs&#47;cloudaudit.googleapis.com&#37;2Factivity&#92;&#34; OR logName:&#92;&#34;&#47;logs&#47;cloudaudit.googleapis.com&#37;2Fsystem_event&#92;&#34;&#34;&#10; type &#61; &#34;logging&#34;&#10; &#125;&#10;&#125;">&#123;&#8230;&#125;</code> | |
| [outputs_location](variables.tf#L202) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | <code>string</code> | | <code>null</code> | |
| [project_parent_ids](variables.tf#L218) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the tenant folder as parent. | <code title="object&#40;&#123;&#10; automation &#61; string&#10; logging &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; automation &#61; null&#10; logging &#61; null&#10;&#125;">&#123;&#8230;&#125;</code> | |
| [test_principal](variables.tf#L301) | Used when testing to bypass the data source returning the current identity. | <code>string</code> | | <code>null</code> | |
| [groups](variables.tf#L139) | Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed. | <code title="object&#40;&#123;&#10; gcp-devops &#61; optional&#40;string&#41;&#10; gcp-network-admins &#61; optional&#40;string&#41;&#10; gcp-security-admins &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | <code>0-bootstrap</code> |
| [iam](variables.tf#L152) | Tenant-level custom IAM settings in role => [principal] format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [iam_bindings_additive](variables.tf#L158) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [locations](variables.tf#L173) | Optional locations for GCS, BigQuery, and logging buckets created here. These are the defaults set at the organization level, and can be overridden via the tenant config variable. | <code title="object&#40;&#123;&#10; bq &#61; string&#10; gcs &#61; string&#10; logging &#61; string&#10; pubsub &#61; list&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; bq &#61; &#34;EU&#34;&#10; gcs &#61; &#34;EU&#34;&#10; logging &#61; &#34;global&#34;&#10; pubsub &#61; &#91;&#93;&#10;&#125;">&#123;&#8230;&#125;</code> | <code>0-bootstrap</code> |
| [log_sinks](variables.tf#L193) | Tenant-level log sinks, in name => {type, filter} format. | <code title="map&#40;object&#40;&#123;&#10; filter &#61; string&#10; type &#61; string&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code title="&#123;&#10; audit-logs &#61; &#123;&#10; filter &#61; &#34;logName:&#92;&#34;&#47;logs&#47;cloudaudit.googleapis.com&#37;2Factivity&#92;&#34; OR logName:&#92;&#34;&#47;logs&#47;cloudaudit.googleapis.com&#37;2Fsystem_event&#92;&#34;&#34;&#10; type &#61; &#34;logging&#34;&#10; &#125;&#10;&#125;">&#123;&#8230;&#125;</code> | |
| [outputs_location](variables.tf#L224) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | <code>string</code> | | <code>null</code> | |
| [project_parent_ids](variables.tf#L240) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the tenant folder as parent. | <code title="object&#40;&#123;&#10; automation &#61; string&#10; logging &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; automation &#61; null&#10; logging &#61; null&#10;&#125;">&#123;&#8230;&#125;</code> | |
| [test_principal](variables.tf#L323) | Used when testing to bypass the data source returning the current identity. | <code>string</code> | | <code>null</code> | |
## Outputs

View File

@ -106,6 +106,7 @@ module "automation-tf-resman-sa-stage2-3" {
}
# assign org policy admin with a tag-based condition to stage 2 and 3 SAs
# TODO: move to new iam_bindings_additive in the organization module
resource "google_organization_iam_member" "org_policy_admin_stage2_3" {
for_each = {

View File

@ -60,6 +60,9 @@ locals {
)
)
}
cicd_sa_resman = try(
module.automation-tf-cicd-sa-bootstrap["0"].iam_email, null
)
}
# tenant bootstrap runs in the org scope and uses top-level automation project
@ -145,10 +148,11 @@ module "automation-tf-org-resman-sa" {
project_id = var.automation.project_id
name = local.resman_sa
service_account_create = false
iam_additive = {
"roles/iam.serviceAccountTokenCreator" = compact([
try(module.automation-tf-cicd-sa-bootstrap["0"].iam_email, null)
])
iam_bindings_additive = local.cicd_sa_resman == null ? {} : {
sa_resman_cicd = {
member = local.cicd_sa_resman
role = "roles/iam.serviceAccountTokenCreator"
}
}
}

View File

@ -95,6 +95,6 @@ module "tenant-folder-iam" {
module.automation-tf-resman-sa.iam_email
]
})
iam_additive = var.iam_additive
depends_on = [module.automation-project]
iam_bindings_additive = var.iam_bindings_additive
depends_on = [module.automation-project]
}

View File

@ -26,19 +26,45 @@ locals {
module "organization" {
source = "../../../modules/organization"
organization_id = "organizations/${var.organization.id}"
iam_additive = merge(
iam_bindings_additive = merge(
{
"roles/resourcemanager.organizationViewer" = [
"group:${local.groups.gcp-admins}"
]
admins_org_viewer = {
member = "group:${local.groups.gcp-admins}"
role = "roles/resourcemanager.organizationViewer"
}
admins_org_policy_admin = {
member = "group:${local.groups.gcp-admins}"
role = "roles/orgpolicy.policyAdmin"
condition = {
title = "org_policy_tag_${var.tenant_config.short_name}_scoped_admins"
description = "Org policy tag scoped grant for tenant ${var.tenant_config.short_name}."
expression = local.iam_tenant_condition
}
}
sa_resman_org_policy_admin = {
member = module.automation-tf-resman-sa.iam_email
role = "roles/orgpolicy.policyAdmin"
condition = {
title = "org_policy_tag_${var.tenant_config.short_name}_scoped_sa_resman"
description = "Org policy tag scoped grant for tenant ${var.tenant_config.short_name}."
expression = local.iam_tenant_condition
}
}
},
local.billing_mode == "org" ? {
"roles/billing.admin" = [
"group:${local.groups.gcp-admins}",
module.automation-tf-resman-sa.iam_email
]
"roles/billing.costsManager" = ["group:${local.groups.gcp-admins}"]
} : {}
local.billing_mode != "org" ? {} : {
admins_billing_admin = {
member = "group:${local.groups.gcp-admins}"
role = "roles/billing.admin"
}
admins_billing_costs_manager = {
member = "group:${local.groups.gcp-admins}"
role = "roles/billing.costsManager"
}
sa_resman_billing_admin = {
member = module.automation-tf-resman-sa.iam_email
role = "roles/billing.admin"
}
}
)
tags = {
tenant = {
@ -50,6 +76,8 @@ module "organization" {
}
}
# TODO: use tag IAM with id in the organization module
resource "google_tags_tag_value_iam_member" "resman_tag_user" {
for_each = var.tag_values
tag_value = each.value
@ -64,21 +92,4 @@ resource "google_tags_tag_value_iam_member" "admins_tag_viewer" {
member = "group:${local.groups.gcp-admins}"
}
# assign org policy admin with a tag-based condition to admin group and stage 1 SA
resource "google_organization_iam_member" "org_policy_admin_stage0" {
for_each = toset([
"group:${local.groups.gcp-admins}",
module.automation-tf-resman-sa.iam_email
])
org_id = var.organization.id
role = "roles/orgpolicy.policyAdmin"
member = each.key
condition {
title = "org_policy_tag_${var.tenant_config.short_name}_scoped"
description = "Org policy tag scoped grant for tenant ${var.tenant_config.short_name}."
expression = local.iam_tenant_condition
}
}
# tag-based condition for service accounts is in the automation-sa file

View File

@ -136,16 +136,38 @@ variable "group_iam" {
default = {}
}
variable "groups" {
# tfdoc:variable:source 0-bootstrap
# https://cloud.google.com/docs/enterprise/setup-checklist
description = "Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed."
type = object({
gcp-devops = optional(string)
gcp-network-admins = optional(string)
gcp-security-admins = optional(string)
})
default = {}
nullable = false
}
variable "iam" {
description = "Tenant-level custom IAM settings in role => [principal] format."
type = map(list(string))
default = {}
}
variable "iam_additive" {
description = "Tenant-level custom IAM settings in role => [principal] format for non-authoritative bindings."
type = map(list(string))
default = {}
variable "iam_bindings_additive" {
description = "Individual additive IAM bindings. Keys are arbitrary."
type = map(object({
member = string
role = string
condition = optional(object({
expression = string
title = string
description = optional(string)
}))
}))
nullable = false
default = {}
}
variable "locations" {

View File

@ -26,16 +26,19 @@ module "root-folder" {
)
name = var.test_skip_data_sources ? "Test" : null
# end test attributes
iam_additive = {
"roles/accesscontextmanager.policyAdmin" = [
local.automation_sas_iam.security
]
"roles/compute.orgFirewallPolicyAdmin" = [
local.automation_sas_iam.networking
]
"roles/compute.xpnAdmin" = [
local.automation_sas_iam.networking
]
iam_bindings_additive = {
sa_net_fw_policy_admin = {
member = local.automation_sas_iam.networking
role = "roles/compute.orgFirewallPolicyAdmin"
}
sa_net_xpn_admin = {
member = local.automation_sas_iam.networking
role = "roles/compute.xpnAdmin"
}
sa_sec_vpcsc_admin = {
member = local.automation_sas_iam.security
role = "roles/accesscontextmanager.policyAdmin"
}
}
org_policies_data_path = var.organization_policy_data_path
}

View File

@ -357,11 +357,20 @@ In code, the distinction above reflects on how IAM bindings are specified in the
This makes it easy to tweak user roles by adding mappings to the `iam_groups` variables of the relevant resources, without having to understand and deal with the details of service account roles.
In those cases where roles need to be assigned to end-user service accounts (e.g. an application or pipeline service account), we offer a stage-level `iam` variable that allows pinpointing individual role/members pairs, without having to touch the code internals, to avoid the risk of breaking a critical role for a robot account. The variable can also be used to assign roles to specific users or to groups external to the organization, e.g. to support external suppliers.
One more critical difference in IAM bindings is between authoritative and additive:
The one exception to this convention is for roles which are part of the delegated grant condition described above, and which can then be assigned from other stages. In this case, use the `iam_additive` variable as they are implemented with non-authoritative resources. Using non-authoritative bindings ensure that re-executing this stage will not override any bindings set in downstream stages.
- authoritative bindings have complete control on principals for a given role; this is the recommended best practice when a single automation actor controls the role, as it removes drift each time Terraform runs
- additive bindings have control only on given role/principal pairs, and need to be used whenever multiple automation actors need to control the role, as is the case for the network user role in Shared VPC setups, and many other situations
A full reference of IAM roles managed by this stage [is available here](./IAM.md).
This stage groups all IAM definitions in the [organization-iam.tf](./organization-iam.tf) file, to allow easy parsing of roles assigned to each group and machine identity.
When customizations are needed, three stage-level variables allow injecting additional bindings to match the desired setup:
- `group_iam` allows adding authoritative bindings for groups
- `iam` allows adding authoritative bindings for any type of supported principal, and is merged with the internal `iam` local and then with group bindings at the module level
- `iam_bindings_additive` allows adding individual role/member pairs, and also supports IAM conditions
Refer to the [project module](../../../modules/project/) for examples on how to use the IAM variables, and they are an interface shared across all our modules.
### Log sinks and log destinations
@ -498,7 +507,8 @@ The remaining configuration is manual, as it regards the repositories themselves
| [identity-providers.tf](./identity-providers.tf) | Workload Identity Federation provider definitions. | | <code>google_iam_workload_identity_pool</code> · <code>google_iam_workload_identity_pool_provider</code> |
| [log-export.tf](./log-export.tf) | Audit log project and sink. | <code>bigquery-dataset</code> · <code>gcs</code> · <code>logging-bucket</code> · <code>project</code> · <code>pubsub</code> | |
| [main.tf](./main.tf) | Module-level locals and resources. | | |
| [organization.tf](./organization.tf) | Organization-level IAM. | <code>organization</code> | <code>google_organization_iam_binding</code> |
| [organization-iam.tf](./organization-iam.tf) | Organization-level IAM bindings locals. | | |
| [organization.tf](./organization.tf) | Organization-level IAM. | <code>organization</code> | |
| [outputs-files.tf](./outputs-files.tf) | Output files persistence to local filesystem. | | <code>local_file</code> |
| [outputs-gcs.tf](./outputs-gcs.tf) | Output files persistence to automation GCS bucket. | | <code>google_storage_bucket_object</code> |
| [outputs.tf](./outputs.tf) | Module outputs. | | |
@ -509,21 +519,22 @@ The remaining configuration is manual, as it regards the repositories themselves
| name | description | type | required | default | producer |
|---|---|:---:|:---:|:---:|:---:|
| [billing_account](variables.tf#L17) | Billing account id. If billing account is not part of the same org set `is_org_level` to `false`. To disable handling of billing IAM roles set `no_iam` to `true`. | <code title="object&#40;&#123;&#10; id &#61; string&#10; is_org_level &#61; optional&#40;bool, true&#41;&#10; no_iam &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | |
| [organization](variables.tf#L206) | Organization details. | <code title="object&#40;&#123;&#10; domain &#61; string&#10; id &#61; number&#10; customer_id &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | |
| [prefix](variables.tf#L221) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | |
| [organization](variables.tf#L219) | Organization details. | <code title="object&#40;&#123;&#10; domain &#61; string&#10; id &#61; number&#10; customer_id &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | |
| [prefix](variables.tf#L234) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | |
| [bootstrap_user](variables.tf#L27) | Email of the nominal user running this stage for the first time. | <code>string</code> | | <code>null</code> | |
| [cicd_repositories](variables.tf#L33) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | <code title="object&#40;&#123;&#10; bootstrap &#61; optional&#40;object&#40;&#123;&#10; branch &#61; string&#10; identity_provider &#61; string&#10; name &#61; string&#10; type &#61; string&#10; &#125;&#41;&#41;&#10; resman &#61; optional&#40;object&#40;&#123;&#10; branch &#61; string&#10; identity_provider &#61; string&#10; name &#61; string&#10; type &#61; string&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> | |
| [custom_role_names](variables.tf#L79) | Names of custom roles defined at the org level. | <code title="object&#40;&#123;&#10; organization_iam_admin &#61; string&#10; service_project_network_admin &#61; string&#10; tenant_network_admin &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; organization_iam_admin &#61; &#34;organizationIamAdmin&#34;&#10; service_project_network_admin &#61; &#34;serviceProjectNetworkAdmin&#34;&#10; tenant_network_admin &#61; &#34;tenantNetworkAdmin&#34;&#10;&#125;">&#123;&#8230;&#125;</code> | |
| [custom_roles](variables.tf#L93) | Map of role names => list of permissions to additionally create at the organization level. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [fast_features](variables.tf#L100) | Selective control for top-level FAST features. | <code title="object&#40;&#123;&#10; data_platform &#61; optional&#40;bool, false&#41;&#10; gke &#61; optional&#40;bool, false&#41;&#10; project_factory &#61; optional&#40;bool, false&#41;&#10; sandbox &#61; optional&#40;bool, false&#41;&#10; teams &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [federated_identity_providers](variables.tf#L113) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | <code title="map&#40;object&#40;&#123;&#10; attribute_condition &#61; optional&#40;string&#41;&#10; issuer &#61; string&#10; custom_settings &#61; optional&#40;object&#40;&#123;&#10; issuer_uri &#61; optional&#40;string&#41;&#10; audiences &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [groups](variables.tf#L132) | Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed. | <code>map&#40;string&#41;</code> | | <code title="&#123;&#10; gcp-billing-admins &#61; &#34;gcp-billing-admins&#34;,&#10; gcp-devops &#61; &#34;gcp-devops&#34;,&#10; gcp-network-admins &#61; &#34;gcp-network-admins&#34;&#10; gcp-organization-admins &#61; &#34;gcp-organization-admins&#34;&#10; gcp-security-admins &#61; &#34;gcp-security-admins&#34;&#10; gcp-support &#61; &#34;gcp-devops&#34;&#10;&#125;">&#123;&#8230;&#125;</code> | |
| [iam](variables.tf#L150) | Organization-level custom IAM settings in role => [principal] format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [iam_additive](variables.tf#L156) | Organization-level custom IAM settings in role => [principal] format for non-authoritative bindings. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [locations](variables.tf#L162) | Optional locations for GCS, BigQuery, and logging buckets created here. | <code title="object&#40;&#123;&#10; bq &#61; string&#10; gcs &#61; string&#10; logging &#61; string&#10; pubsub &#61; list&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; bq &#61; &#34;EU&#34;&#10; gcs &#61; &#34;EU&#34;&#10; logging &#61; &#34;global&#34;&#10; pubsub &#61; &#91;&#93;&#10;&#125;">&#123;&#8230;&#125;</code> | |
| [log_sinks](variables.tf#L181) | Org-level log sinks, in name => {type, filter} format. | <code title="map&#40;object&#40;&#123;&#10; filter &#61; string&#10; type &#61; string&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code title="&#123;&#10; audit-logs &#61; &#123;&#10; filter &#61; &#34;logName:&#92;&#34;&#47;logs&#47;cloudaudit.googleapis.com&#37;2Factivity&#92;&#34; OR logName:&#92;&#34;&#47;logs&#47;cloudaudit.googleapis.com&#37;2Fsystem_event&#92;&#34;&#34;&#10; type &#61; &#34;logging&#34;&#10; &#125;&#10; vpc-sc &#61; &#123;&#10; filter &#61; &#34;protoPayload.metadata.&#64;type&#61;&#92;&#34;type.googleapis.com&#47;google.cloud.audit.VpcServiceControlAuditMetadata&#92;&#34;&#34;&#10; type &#61; &#34;logging&#34;&#10; &#125;&#10;&#125;">&#123;&#8230;&#125;</code> | |
| [outputs_location](variables.tf#L215) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | <code>string</code> | | <code>null</code> | |
| [project_parent_ids](variables.tf#L230) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | <code title="object&#40;&#123;&#10; automation &#61; string&#10; billing &#61; string&#10; logging &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; automation &#61; null&#10; billing &#61; null&#10; logging &#61; null&#10;&#125;">&#123;&#8230;&#125;</code> | |
| [group_iam](variables.tf#L132) | Organization-level authoritative IAM binding for groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [groups](variables.tf#L140) | Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed. | <code>map&#40;string&#41;</code> | | <code title="&#123;&#10; gcp-billing-admins &#61; &#34;gcp-billing-admins&#34;,&#10; gcp-devops &#61; &#34;gcp-devops&#34;,&#10; gcp-network-admins &#61; &#34;gcp-network-admins&#34;&#10; gcp-organization-admins &#61; &#34;gcp-organization-admins&#34;&#10; gcp-security-admins &#61; &#34;gcp-security-admins&#34;&#10; gcp-support &#61; &#34;gcp-devops&#34;&#10;&#125;">&#123;&#8230;&#125;</code> | |
| [iam](variables.tf#L158) | Organization-level custom IAM settings in role => [principal] format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [iam_bindings_additive](variables.tf#L165) | Organization-level custom additive IAM bindings. Keys are arbitrary. | <code title="map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| [locations](variables.tf#L180) | Optional locations for GCS, BigQuery, and logging buckets created here. | <code title="object&#40;&#123;&#10; bq &#61; optional&#40;string, &#34;EU&#34;&#41;&#10; gcs &#61; optional&#40;string, &#34;EU&#34;&#41;&#10; logging &#61; optional&#40;string, &#34;global&#34;&#41;&#10; pubsub &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [log_sinks](variables.tf#L194) | Org-level log sinks, in name => {type, filter} format. | <code title="map&#40;object&#40;&#123;&#10; filter &#61; string&#10; type &#61; string&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code title="&#123;&#10; audit-logs &#61; &#123;&#10; filter &#61; &#34;logName:&#92;&#34;&#47;logs&#47;cloudaudit.googleapis.com&#37;2Factivity&#92;&#34; OR logName:&#92;&#34;&#47;logs&#47;cloudaudit.googleapis.com&#37;2Fsystem_event&#92;&#34;&#34;&#10; type &#61; &#34;logging&#34;&#10; &#125;&#10; vpc-sc &#61; &#123;&#10; filter &#61; &#34;protoPayload.metadata.&#64;type&#61;&#92;&#34;type.googleapis.com&#47;google.cloud.audit.VpcServiceControlAuditMetadata&#92;&#34;&#34;&#10; type &#61; &#34;logging&#34;&#10; &#125;&#10;&#125;">&#123;&#8230;&#125;</code> | |
| [outputs_location](variables.tf#L228) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | <code>string</code> | | <code>null</code> | |
| [project_parent_ids](variables.tf#L243) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | <code title="object&#40;&#123;&#10; automation &#61; string&#10; billing &#61; string&#10; logging &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; automation &#61; null&#10; billing &#61; null&#10; logging &#61; null&#10;&#125;">&#123;&#8230;&#125;</code> | |
## Outputs

View File

@ -16,6 +16,10 @@
# tfdoc:file:description Automation project and resources.
locals {
cicd_resman_sa = try(module.automation-tf-cicd-sa["resman"].iam_email, "")
}
module "automation-project" {
source = "../../../modules/project"
billing_account = var.billing_account.id
@ -151,11 +155,14 @@ module "automation-tf-resman-sa" {
prefix = local.prefix
# allow SA used by CI/CD workflow to impersonate this SA
# we use additive IAM to allow tenant CI/CD SAs to impersonate it
iam_additive = {
"roles/iam.serviceAccountTokenCreator" = compact([
try(module.automation-tf-cicd-sa["resman"].iam_email, null)
])
}
iam_bindings_additive = (
local.cicd_resman_sa == "" ? {} : {
cicd_token_creator = {
member = local.cicd_resman_sa
role = "roles/iam.serviceAccountTokenCreator"
}
}
)
iam_storage_roles = {
(module.automation-tf-output-gcs.name) = ["roles/storage.admin"]
}

View File

@ -0,0 +1,153 @@
/**
* Copyright 2023 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.
*/
# tfdoc:file:description Organization-level IAM bindings locals.
locals {
# IAM roles in the org to reset (remove principals)
iam_delete_roles = [
"roles/billing.creator"
]
# domain IAM bindings
iam_domain_bindings = {
"domain:${var.organization.domain}" = {
authoritative = ["roles/browser"]
additive = []
}
}
# human (groups) IAM bindings
iam_group_bindings = {
(local.groups.gcp-billing-admins) = {
authoritative = []
additive = (
local.billing_mode != "org" ? [] : [
"roles/billing.admin",
"roles/billing.costsManager"
]
)
}
(local.groups.gcp-network-admins) = {
authoritative = [
"roles/cloudasset.owner",
"roles/cloudsupport.techSupportEditor",
]
additive = [
"roles/compute.orgFirewallPolicyAdmin",
"roles/compute.xpnAdmin"
]
}
(local.groups.gcp-organization-admins) = {
authoritative = [
"roles/cloudasset.owner",
"roles/cloudsupport.admin",
"roles/compute.osAdminLogin",
"roles/compute.osLoginExternalUser",
"roles/owner",
"roles/resourcemanager.folderAdmin",
"roles/resourcemanager.organizationAdmin",
"roles/resourcemanager.projectCreator",
]
additive = concat(
[
"roles/orgpolicy.policyAdmin"
],
local.billing_mode != "org" ? [] : [
"roles/billing.admin",
"roles/billing.costsManager"
]
)
}
(local.groups.gcp-security-admins) = {
authoritative = [
"roles/cloudasset.owner",
"roles/cloudsupport.techSupportEditor",
"roles/iam.securityReviewer",
"roles/logging.admin",
"roles/securitycenter.admin",
]
additive = [
"roles/accesscontextmanager.policyAdmin",
"roles/iam.organizationRoleAdmin",
"roles/orgpolicy.policyAdmin"
]
}
(local.groups.gcp-support) = {
authoritative = [
"roles/cloudsupport.techSupportEditor",
"roles/logging.viewer",
"roles/monitoring.viewer",
]
additive = []
}
}
# machine (service accounts) IAM bindings, in logical format
# the service account module's "magic" outputs allow us to use dynamic values
iam_sa_bindings = {
(module.automation-tf-bootstrap-sa.iam_email) = {
authoritative = [
"roles/logging.admin",
"roles/resourcemanager.organizationAdmin",
"roles/resourcemanager.projectCreator",
"roles/resourcemanager.projectMover",
]
additive = concat(
[
"roles/iam.organizationRoleAdmin",
"roles/orgpolicy.policyAdmin"
],
local.billing_mode != "org" ? [] : [
"roles/billing.admin",
"roles/billing.costsManager"
]
)
}
(module.automation-tf-resman-sa.iam_email) = {
authoritative = [
"roles/logging.admin",
"roles/resourcemanager.folderAdmin",
"roles/resourcemanager.projectCreator",
"roles/resourcemanager.tagAdmin",
"roles/resourcemanager.tagUser"
]
additive = concat(
[
"roles/orgpolicy.policyAdmin"
],
local.billing_mode != "org" ? [] : [
"roles/billing.admin",
"roles/billing.costsManager"
]
)
}
}
# bootstrap user bindings
iam_user_bootstrap_bindings = var.bootstrap_user == null ? {} : {
"user:${var.bootstrap_user}" = {
authoritative = [
"roles/logging.admin",
"roles/owner",
"roles/resourcemanager.organizationAdmin",
"roles/resourcemanager.projectCreator"
]
additive = (
local.billing_mode != "org" ? [] : [
"roles/billing.admin",
"roles/billing.costsManager"
]
)
}
}
}

View File

@ -17,111 +17,51 @@
# tfdoc:file:description Organization-level IAM.
locals {
# organization authoritative IAM bindings, in an easy to edit format before
# they are combined with var.iam a bit further in locals
_iam = {
"roles/billing.creator" = []
"roles/browser" = [
"domain:${var.organization.domain}"
]
"roles/logging.admin" = concat(
[
module.automation-tf-bootstrap-sa.iam_email,
module.automation-tf-resman-sa.iam_email
],
local._iam_bootstrap_user
)
"roles/owner" = local._iam_bootstrap_user
"roles/resourcemanager.folderAdmin" = [
module.automation-tf-resman-sa.iam_email
]
"roles/resourcemanager.organizationAdmin" = concat(
[module.automation-tf-bootstrap-sa.iam_email],
local._iam_bootstrap_user
)
"roles/resourcemanager.projectCreator" = concat(
[
module.automation-tf-bootstrap-sa.iam_email,
module.automation-tf-resman-sa.iam_email
],
local._iam_bootstrap_user
)
"roles/resourcemanager.projectMover" = [
module.automation-tf-bootstrap-sa.iam_email
]
"roles/resourcemanager.tagAdmin" = [
module.automation-tf-resman-sa.iam_email
]
"roles/resourcemanager.tagUser" = [
module.automation-tf-resman-sa.iam_email
]
}
# organization additive IAM bindings, in an easy to edit format before
# they are combined with var.iam_additive a bit further in locals
_iam_additive = merge(
# reassemble logical bindings into the formats expected by the module
_iam_bindings = merge(
local.iam_domain_bindings,
local.iam_sa_bindings,
local.iam_user_bootstrap_bindings,
{
"roles/accesscontextmanager.policyAdmin" = [
local.groups_iam.gcp-security-admins
]
"roles/compute.orgFirewallPolicyAdmin" = [
local.groups_iam.gcp-network-admins
]
"roles/compute.xpnAdmin" = [
local.groups_iam.gcp-network-admins
]
# use additive to support cross-org roles for billing
"roles/iam.organizationRoleAdmin" = [
# uncomment if roles/owner is removed to organization admins
# local.groups.gcp-organization-admins,
local.groups_iam.gcp-security-admins,
module.automation-tf-bootstrap-sa.iam_email
]
"roles/orgpolicy.policyAdmin" = [
local.groups_iam.gcp-organization-admins,
local.groups_iam.gcp-security-admins,
module.automation-tf-resman-sa.iam_email
]
# the following is useful if roles/browser is not desirable
# "roles/resourcemanager.organizationViewer" = [
# "domain:${var.organization.domain}"
# ]
for k, v in local.iam_group_bindings : "group:${k}" => {
authoritative = []
additive = v.additive
}
}
)
_iam_bindings_auth = flatten([
for member, data in local._iam_bindings : [
for role in data.authoritative : {
member = member
role = role
}
]
])
_iam_bindings_add = flatten([
for member, data in local._iam_bindings : [
for role in data.additive : {
member = member
role = role
}
]
])
group_iam = {
for k, v in local.iam_group_bindings : k => v.authoritative
}
iam = merge(
{
for r in local.iam_delete_roles : r => []
},
local.billing_mode == "org" ? {
"roles/billing.admin" = [
local.groups_iam.gcp-billing-admins,
local.groups_iam.gcp-organization-admins,
module.automation-tf-bootstrap-sa.iam_email,
module.automation-tf-resman-sa.iam_email
],
"roles/billing.costsManager" = [
local.groups_iam.gcp-billing-admins,
local.groups_iam.gcp-organization-admins,
module.automation-tf-bootstrap-sa.iam_email,
module.automation-tf-resman-sa.iam_email
]
} : {}
{
for b in local._iam_bindings_auth : b.role => b.member...
}
)
_iam_bootstrap_user = (
var.bootstrap_user == null ? [] : ["user:${var.bootstrap_user}"]
)
iam = {
for role in local.iam_roles : role => distinct(concat(
try(sort(local._iam[role]), []),
try(sort(var.iam[role]), [])
))
iam_bindings_additive = {
for b in local._iam_bindings_add : "${b.role}-${b.member}" => {
member = b.member
role = b.role
}
}
iam_additive = {
for role in local.iam_roles_additive : role => distinct(concat(
try(sort(local._iam_additive[role]), []),
try(sort(var.iam_additive[role]), [])
))
}
iam_roles = distinct(concat(
keys(local._iam), keys(var.iam)
))
iam_roles_additive = distinct(concat(
keys(local._iam_additive), keys(var.iam_additive)
))
}
module "organization" {
@ -129,40 +69,52 @@ module "organization" {
organization_id = "organizations/${var.organization.id}"
# human (groups) IAM bindings
group_iam = {
(local.groups.gcp-organization-admins) = [
"roles/cloudasset.owner",
"roles/cloudsupport.admin",
"roles/compute.osAdminLogin",
"roles/compute.osLoginExternalUser",
"roles/owner",
# granted via additive roles
# roles/iam.organizationRoleAdmin
# roles/orgpolicy.policyAdmin
"roles/resourcemanager.folderAdmin",
"roles/resourcemanager.organizationAdmin",
"roles/resourcemanager.projectCreator",
],
(local.groups.gcp-network-admins) = [
"roles/cloudasset.owner",
"roles/cloudsupport.techSupportEditor",
]
(local.groups.gcp-security-admins) = [
"roles/cloudasset.owner",
"roles/cloudsupport.techSupportEditor",
"roles/iam.securityReviewer",
"roles/logging.admin",
"roles/securitycenter.admin",
],
(local.groups.gcp-support) = [
"roles/cloudsupport.techSupportEditor",
"roles/logging.viewer",
"roles/monitoring.viewer",
]
for k, v in local.group_iam :
k => distinct(concat(v, lookup(var.group_iam, k, [])))
}
# machine (service accounts) IAM bindings
iam = local.iam
iam = merge(
{
for k, v in local.iam : k => distinct(concat(v, lookup(var.iam, k, [])))
},
{
for k, v in var.iam : k => v if lookup(local.iam, k, null) == null
}
)
# additive bindings, used for roles co-managed by different stages
iam_additive = local.iam_additive
iam_bindings_additive = merge(
local.iam_bindings_additive,
var.iam_bindings_additive
)
# delegated role grant for resource manager service account
iam_bindings = {
sa_resman_delegated_iam = {
members = [module.automation-tf-resman-sa.iam_email]
role = module.organization.custom_role_id[var.custom_role_names.organization_iam_admin]
condition = {
expression = format(
"api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])",
join(",", formatlist("'%s'", concat(
[
"roles/accesscontextmanager.policyAdmin",
"roles/compute.orgFirewallPolicyAdmin",
"roles/compute.xpnAdmin",
"roles/orgpolicy.policyAdmin",
"roles/resourcemanager.organizationViewer",
module.organization.custom_role_id[var.custom_role_names.tenant_network_admin]
],
local.billing_mode == "org" ? [
"roles/billing.admin",
"roles/billing.costsManager",
"roles/billing.user",
] : []
)))
)
title = "automation_sa_delegated_grants"
description = "Automation service account delegated grants."
}
}
}
custom_roles = merge(var.custom_roles, {
# this is needed for use in additive IAM bindings, to avoid conflicts
(var.custom_role_names.organization_iam_admin) = [
@ -200,36 +152,3 @@ module "organization" {
}
}
}
# assign the custom restricted Organization Admin role to the relevant service
# accounts, with a condition that only enables granting specific roles;
# these roles use additive bindings everywhere to avoid conflicts / permadiffs
resource "google_organization_iam_binding" "org_admin_delegated" {
org_id = var.organization.id
role = module.organization.custom_role_id[var.custom_role_names.organization_iam_admin]
members = [module.automation-tf-resman-sa.iam_email]
condition {
title = "automation_sa_delegated_grants"
description = "Automation service account delegated grants."
expression = format(
"api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])",
join(",", formatlist("'%s'", concat(
[
"roles/accesscontextmanager.policyAdmin",
"roles/compute.orgFirewallPolicyAdmin",
"roles/compute.xpnAdmin",
"roles/orgpolicy.policyAdmin",
"roles/resourcemanager.organizationViewer",
module.organization.custom_role_id[var.custom_role_names.tenant_network_admin]
],
local.billing_mode == "org" ? [
"roles/billing.admin",
"roles/billing.costsManager",
"roles/billing.user",
] : []
)))
)
}
depends_on = [module.organization]
}

View File

@ -129,6 +129,14 @@ variable "federated_identity_providers" {
# }
}
variable "group_iam" {
description = "Organization-level authoritative IAM binding for groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable."
type = map(list(string))
default = {}
nullable = false
}
variable "groups" {
# https://cloud.google.com/docs/enterprise/setup-checklist
description = "Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed."
@ -150,30 +158,35 @@ variable "groups" {
variable "iam" {
description = "Organization-level custom IAM settings in role => [principal] format."
type = map(list(string))
nullable = false
default = {}
}
variable "iam_additive" {
description = "Organization-level custom IAM settings in role => [principal] format for non-authoritative bindings."
type = map(list(string))
default = {}
variable "iam_bindings_additive" {
description = "Organization-level custom additive IAM bindings. Keys are arbitrary."
type = map(object({
member = string
role = string
condition = optional(object({
expression = string
title = string
description = optional(string)
}))
}))
nullable = false
default = {}
}
variable "locations" {
description = "Optional locations for GCS, BigQuery, and logging buckets created here."
type = object({
bq = string
gcs = string
logging = string
pubsub = list(string)
bq = optional(string, "EU")
gcs = optional(string, "EU")
logging = optional(string, "global")
pubsub = optional(list(string), [])
})
default = {
bq = "EU"
gcs = "EU"
logging = "global"
pubsub = []
}
nullable = false
default = {}
}
# See https://cloud.google.com/architecture/exporting-stackdriver-logging-for-security-and-access-analytics

View File

@ -342,11 +342,11 @@ Due to its simplicity, this stage lends itself easily to customizations: adding
| [branch-data-platform.tf](./branch-data-platform.tf) | Data Platform stages resources. | <code>folder</code> · <code>gcs</code> · <code>iam-service-account</code> | <code>google_organization_iam_member</code> |
| [branch-gke.tf](./branch-gke.tf) | GKE multitenant stage resources. | <code>folder</code> · <code>gcs</code> · <code>iam-service-account</code> | |
| [branch-networking.tf](./branch-networking.tf) | Networking stage resources. | <code>folder</code> · <code>gcs</code> · <code>iam-service-account</code> | |
| [branch-project-factory.tf](./branch-project-factory.tf) | Project factory stage resources. | <code>gcs</code> · <code>iam-service-account</code> | <code>google_organization_iam_member</code> |
| [branch-project-factory.tf](./branch-project-factory.tf) | Project factory stage resources. | <code>gcs</code> · <code>iam-service-account</code> | |
| [branch-sandbox.tf](./branch-sandbox.tf) | Sandbox stage resources. | <code>folder</code> · <code>gcs</code> · <code>iam-service-account</code> | <code>google_organization_iam_member</code> |
| [branch-security.tf](./branch-security.tf) | Security stage resources. | <code>folder</code> · <code>gcs</code> · <code>iam-service-account</code> | |
| [branch-teams.tf](./branch-teams.tf) | Team stage resources. | <code>folder</code> · <code>gcs</code> · <code>iam-service-account</code> | |
| [branch-tenants.tf](./branch-tenants.tf) | Lightweight tenant resources. | <code>folder</code> · <code>gcs</code> · <code>iam-service-account</code> · <code>organization</code> · <code>project</code> | |
| [branch-tenants.tf](./branch-tenants.tf) | Lightweight tenant resources. | <code>folder</code> · <code>gcs</code> · <code>iam-service-account</code> · <code>project</code> | |
| [cicd-data-platform.tf](./cicd-data-platform.tf) | CI/CD resources for the data platform branch. | <code>iam-service-account</code> · <code>source-repository</code> | |
| [cicd-gke.tf](./cicd-gke.tf) | CI/CD resources for the data platform branch. | <code>iam-service-account</code> · <code>source-repository</code> | |
| [cicd-networking.tf](./cicd-networking.tf) | CI/CD resources for the networking branch. | <code>iam-service-account</code> · <code>source-repository</code> | |
@ -354,6 +354,7 @@ Due to its simplicity, this stage lends itself easily to customizations: adding
| [cicd-security.tf](./cicd-security.tf) | CI/CD resources for the security branch. | <code>iam-service-account</code> · <code>source-repository</code> | |
| [cicd-teams.tf](./cicd-teams.tf) | CI/CD resources for individual teams. | <code>iam-service-account</code> · <code>source-repository</code> | |
| [main.tf](./main.tf) | Module-level locals and resources. | | |
| [organization-iam.tf](./organization-iam.tf) | Organization-level IAM bindings locals. | | |
| [organization.tf](./organization.tf) | Organization policies. | <code>organization</code> | |
| [outputs-files.tf](./outputs-files.tf) | Output files persistence to local filesystem. | | <code>local_file</code> |
| [outputs-gcs.tf](./outputs-gcs.tf) | Output files persistence to automation GCS bucket. | | <code>google_storage_bucket_object</code> |

View File

@ -79,33 +79,3 @@ module "branch-pf-prod-gcs" {
"roles/storage.objectAdmin" = [module.branch-pf-prod-sa.0.iam_email]
}
}
resource "google_organization_iam_member" "org_policy_admin_pf_dev" {
count = var.fast_features.project_factory ? 1 : 0
org_id = var.organization.id
role = "roles/orgpolicy.policyAdmin"
member = module.branch-pf-dev-sa.0.iam_email
condition {
title = "org_policy_tag_pf_scoped_dev"
description = "Org policy tag scoped grant for project factory dev."
expression = <<-END
resource.matchTag('${var.organization.id}/${var.tag_names.context}', 'teams')
&&
resource.matchTag('${var.organization.id}/${var.tag_names.environment}', 'development')
END
}
}
resource "google_organization_iam_member" "org_policy_admin_pf_prod" {
count = var.fast_features.project_factory ? 1 : 0
org_id = var.organization.id
role = "roles/orgpolicy.policyAdmin"
member = module.branch-pf-prod-sa.0.iam_email
condition {
title = "org_policy_tag_pf_scoped_prod"
description = "Org policy tag scoped grant for project factory prod."
expression = <<-END
resource.matchTag('${var.organization.id}/${var.tag_names.context}', 'teams')
END
}
}

View File

@ -23,27 +23,6 @@ locals {
module.tenant-self-iac-sa[k].iam_email
]
}
tenant_org_iam = compact(flatten([
for k, v in var.tenants : [
"group:${v.admin_group_email}",
v.organization != null ? "domain:${v.organization.domain}" : null
]
]))
}
# org-level roles for each tenant (additive)
module "tenant-org-iam" {
source = "../../../modules/organization"
organization_id = "organizations/${var.organization.id}"
iam_additive = {
"roles/compute.osLoginExternalUser" = [
for k, v in var.tenants :
"domain:${v.organization.domain}" if v.organization != null
]
"roles/resourcemanager.organizationViewer" = local.tenant_org_iam
}
}
# top-level "Tenants" folder
@ -107,7 +86,9 @@ module "tenant-core-folder-iam" {
folder_create = false
iam = merge(
{
"roles/owner" = [module.tenant-core-sa[each.key].iam_email]
"roles/owner" = [
module.tenant-core-sa[each.key].iam_email
]
"roles/viewer" = local.tenant_iam[each.key]
},
{

View File

@ -0,0 +1,141 @@
/**
* Copyright 2023 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.
*/
# tfdoc:file:description Organization-level IAM bindings locals.
locals {
iam_bindings_additive = merge(
# network and security
{
sa_net_fw_policy_admin = {
member = module.branch-network-sa.iam_email
role = "roles/compute.orgFirewallPolicyAdmin"
}
sa_net_xpn_admin = {
member = module.branch-network-sa.iam_email
role = "roles/compute.xpnAdmin"
}
sa_sec_vpcsc_admin = {
member = module.branch-security-sa.iam_email
role = "roles/accesscontextmanager.policyAdmin"
}
},
# optional billing roles for network and security
local.billing_mode != "org" ? {} : {
sa_net_billing = {
member = module.branch-network-sa.iam_email
role = "roles/billing.user"
}
sa_sec_billing = {
member = module.branch-security-sa.iam_email
role = "roles/billing.user"
}
},
# optional billing roles for data platform
local.billing_mode != "org" || !var.fast_features.data_platform ? {} : {
sa_dp_dev_billing = {
member = module.branch-dp-dev-sa.0.iam_email
role = "roles/billing.user"
}
sa_dp_prod_billing = {
member = module.branch-dp-prod-sa.0.iam_email
role = "roles/billing.user"
}
},
# optional billing roles for GKE
local.billing_mode != "org" || !var.fast_features.gke ? {} : {
sa_gke_dev_billing = {
member = module.branch-gke-dev-sa.0.iam_email
role = "roles/billing.user"
}
sa_gke_prod_billing = {
member = module.branch-gke-prod-sa.0.iam_email
role = "roles/billing.user"
}
},
# optional billing roles for project factory
local.billing_mode != "org" || !var.fast_features.project_factory ? {} : {
sa_pf_dev_billing = {
member = module.branch-pf-dev-sa.0.iam_email
role = "roles/billing.user"
}
sa_pf_dev_costs_manager = {
member = module.branch-pf-dev-sa.0.iam_email
role = "roles/billing.costsManager"
}
sa_pf_prod_billing = {
member = module.branch-pf-prod-sa.0.iam_email
role = "roles/billing.user"
}
sa_pf_prod_costs_manager = {
member = module.branch-pf-prod-sa.0.iam_email
role = "roles/billing.costsManager"
}
},
# scoped org policy admin grants for project factory
!var.fast_features.project_factory ? {} : {
sa_pf_dev_conditional_org_policy = {
member = module.branch-pf-dev-sa.0.iam_email
role = "roles/orgpolicy.policyAdmin"
condition = {
title = "org_policy_tag_pf_scoped_dev"
description = "Org policy tag scoped grant for project factory dev."
expression = <<-END
resource.matchTag('${var.organization.id}/${var.tag_names.context}', 'teams')
&&
resource.matchTag('${var.organization.id}/${var.tag_names.environment}', 'development')
END
}
}
sa_pf_prod_conditional_org_policy = {
member = module.branch-pf-prod-sa.0.iam_email
role = "roles/orgpolicy.policyAdmin"
condition = {
title = "org_policy_tag_pf_scoped_prod"
description = "Org policy tag scoped grant for project factory prod."
expression = <<-END
resource.matchTag('${var.organization.id}/${var.tag_names.context}', 'teams')
END
}
}
},
# lightweight tenant roles
{
for k, v in var.tenants : "oslogin_ext_user-tenant_${k}" => {
member = "domain:${v.organization.domain}"
role = "roles/compute.osLoginExternalUser"
} if v.organization != null
},
{
for k, v in var.tenants : "org-viewer-tenant_${k}_domain" => {
member = "domain:${v.organization.domain}"
role = "roles/resourcemanager.organizationViewer"
} if v.organization != null
},
{
for k, v in var.tenants : "org-viewer-tenant_${k}_admin" => {
member = "group:${v.admin_group_email}"
role = "roles/resourcemanager.organizationViewer"
}
},
local.billing_mode != "org" ? {} : {
for k, v in var.tenants : "billing_user-tenant_${k}_billing_admin" => {
member = "group:${v.admin_group_email}"
role = "roles/billing.user"
}
},
)
}

View File

@ -49,39 +49,8 @@ locals {
module "organization" {
source = "../../../modules/organization"
organization_id = "organizations/${var.organization.id}"
# IAM additive bindings, granted via the restricted Organization Admin custom
# role assigned in stage 00; they need to be additive to avoid conflicts
iam_additive = merge(
{
"roles/accesscontextmanager.policyAdmin" = [
module.branch-security-sa.iam_email
]
"roles/compute.orgFirewallPolicyAdmin" = [
module.branch-network-sa.iam_email
]
"roles/compute.xpnAdmin" = [
module.branch-network-sa.iam_email
]
},
local.billing_mode == "org" ? {
"roles/billing.costsManager" = concat(
local.branch_optional_sa_lists.pf-dev,
local.branch_optional_sa_lists.pf-prod
)
"roles/billing.user" = concat(
[
module.branch-network-sa.iam_email,
module.branch-security-sa.iam_email,
],
local.branch_optional_sa_lists.dp-dev,
local.branch_optional_sa_lists.dp-prod,
local.branch_optional_sa_lists.gke-dev,
local.branch_optional_sa_lists.gke-prod,
local.branch_optional_sa_lists.pf-dev,
local.branch_optional_sa_lists.pf-prod,
)
} : {}
)
# additive bindings via delegated IAM grant set in stage 0
iam_bindings_additive = local.iam_bindings_additive
# sample subset of useful organization policies, edit to suit requirements
org_policies = {
"iam.allowedPolicyMemberDomains" = {
@ -116,53 +85,48 @@ module "organization" {
org_policies_data_path = "${var.data_dir}/org-policies"
# do not assign tagViewer or tagUser roles here on tag keys and values as
# they are managed authoritatively and will break multitenant stages
tags = merge(
local.tags,
{
(var.tag_names.context) = {
description = "Resource management context."
iam = {}
values = {
data = null
gke = null
networking = null
sandbox = null
security = null
teams = null
tenant = null
}
tags = merge(local.tags, {
(var.tag_names.context) = {
description = "Resource management context."
iam = {}
values = {
data = null
gke = null
networking = null
sandbox = null
security = null
teams = null
tenant = null
}
(var.tag_names.environment) = {
description = "Environment definition."
iam = {}
values = {
development = null
production = null
}
}
(var.tag_names.environment) = {
description = "Environment definition."
iam = {}
values = {
development = null
production = null
}
(var.tag_names.org-policies) = {
description = "Organization policy conditions."
iam = {}
values = {
allowed-policy-member-domains-all = merge({}, try(
local.tags[var.tag_names.org-policies].values.allowed-policy-member-domains-all,
{}
))
}
}
(var.tag_names.org-policies) = {
description = "Organization policy conditions."
iam = {}
values = {
allowed-policy-member-domains-all = merge({}, try(
local.tags[var.tag_names.org-policies].values.allowed-policy-member-domains-all,
{}
))
}
(var.tag_names.tenant) = {
description = "Organization tenant."
values = {
for k, v in var.tenants : k => {
description = v.descriptive_name
iam = {
"roles/resourcemanager.tagViewer" = local.tenant_iam[k]
}
}
(var.tag_names.tenant) = {
description = "Organization tenant."
values = {
for k, v in var.tenants : k => {
description = v.descriptive_name
iam = {
"roles/resourcemanager.tagViewer" = local.tenant_iam[k]
}
}
}
}
)
})
}
# organization policy conditional roles are in the relevant branch files

View File

@ -46,7 +46,7 @@ module "dev-sec-kms" {
name = "dev-${each.key}"
}
# rename to `key_iam` to switch to authoritative bindings
key_iam_additive = {
key_iam = {
for k, v in local.kms_locations_keys[each.key] : k => v.iam
}
keys = local.kms_locations_keys[each.key]

View File

@ -45,7 +45,7 @@ module "prod-sec-kms" {
name = "prod-${each.key}"
}
# rename to `key_iam` to switch to authoritative bindings
key_iam_additive = {
key_iam = {
for k, v in local.kms_locations_keys[each.key] : k => v.iam
}
keys = local.kms_locations_keys[each.key]

View File

@ -13,43 +13,13 @@ A single factory creates projects in a well-defined context, according to your r
Projects for each environment across different teams are created by dedicated service accounts, as exemplified in the diagram above. While there's no intrinsic limitation regarding where the project factory can create a projects, the IAM bindings for the service account effectively enforce boundaries (e.g., the production service account shouldn't be able to create or have any access to the development projects, and vice versa).
The project factory takes care of the following activities:
- Project creation
- API/Services enablement
- Service accounts creation
- IAM roles assignment for groups and service accounts
- KMS keys roles assignment
- Shared VPC attachment and subnets IAM binding
- DNS zones creation and visibility configuration
- Project-level org policies definition
- Billing setup (billing account attachment and budget configuration)
- Essential contacts definition (for [budget alerts](https://cloud.google.com/billing/docs/how-to/budgets) and [important notifications](https://cloud.google.com/resource-manager/docs/managing-notification-contacts?hl=en))
The project factory exposes all the features of the underlying [project module](../../../../modules/project/), including Shared VPC service project attachment, VPC SC perimeter membership, etc.
## How to run this stage
This stage is meant to be executed after "foundational stages" (i.e., stages [`00-bootstrap`](../../0-bootstrap), [`01-resman`](../../1-resman), 02-networking (either [VPN](../../2-networking-b-vpn), [NVA](../../2-networking-c-nva), [NVA with BGP support](../../2-networking-e-nva-bgp)) and [`02-security`](../../2-security)) have been run.
It's of course possible to run this stage in isolation, by making sure the architectural prerequisites are satisfied (e.g., networking), and that the Service Account running the stage is granted the roles/permissions below:
- One service account per environment, each with appropriate permissions
- at the organization level a custom role for networking operations including the following permissions
- `"compute.organizations.enableXpnResource"`,
- `"compute.organizations.disableXpnResource"`,
- `"compute.subnetworks.setIamPolicy"`,
- `"dns.networks.bindPrivateDNSZone"`
- and role `"roles/orgpolicy.policyAdmin"`
- on each folder where projects are created
- `"roles/logging.admin"`
- `"roles/owner"`
- `"roles/resourcemanager.folderAdmin"`
- `"roles/resourcemanager.projectCreator"`
- on the host project for the Shared VPC
- `"roles/browser"`
- `"roles/compute.viewer"`
- `"roles/dns.admin"`
- If networking is used (e.g., for VMs, GKE Clusters or AppEngine flex), VPC Host projects and their subnets should exist when creating projects
- If per-environment DNS sub-zones are required, one "root" zone per environment should exist when creating projects (e.g., dev.gcp.example.com.)
It's of course possible to run this stage in isolation, by making sure the architectural prerequisites are satisfied (e.g., networking), and that the Service Account running the stage is granted the appropriate roles.
### Provider and Terraform variables
@ -83,14 +53,11 @@ gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/2-networking.aut
gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/2-security.auto.tfvars.json ./
```
If you're not using Fast, refer to the [Variables](#variables) table at the bottom of this document for a full list of variables, their origin (e.g., a stage or specific to this one), and descriptions explaining their meaning.
If you're not using FAST, refer to the [Variables](#variables) table at the bottom of this document for a full list of variables, their origin (e.g., a stage or specific to this one), and descriptions explaining their meaning.
Besides the values above, a project factory takes 2 additional inputs:
Besides the values above, the project factory is drive by data files, with one file per project.
- `data/defaults.yaml`, manually configured by adapting the [`data/defaults.yaml`](./data/defaults.yaml), which defines per-environment default values e.g., for billing alerts and labels.
- `data/projects/*.yaml`, one file per project (optionally grouped in folders), which configures each project. A [`data/projects/project.yaml`](./data/projects/project.yaml.sample) is provided as reference and documentation for the schema. Projects will be named after the filename, e.g., `fast-dev-lab0.yaml` will create project `fast-dev-lab0`.
Once the configuration is complete, run the project factory by running
Once the configuration is complete, run the project factory with:
```bash
terraform init
@ -99,7 +66,6 @@ terraform apply
<!-- TFDOC OPTS files:1 show_extra:1 -->
<!-- BEGIN TFDOC -->
## Files
| name | description | modules |
@ -113,17 +79,13 @@ terraform apply
| name | description | type | required | default | producer |
|---|---|:---:|:---:|:---:|:---:|
| [billing_account](variables.tf#L19) | 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> |
| [prefix](variables.tf#L60) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
| [data_dir](variables.tf#L32) | Relative path for the folder storing configuration data. | <code>string</code> | | <code>&#34;data&#47;projects&#34;</code> | |
| [defaults_file](variables.tf#L38) | Relative path for the file storing the project factory configuration. | <code>string</code> | | <code>&#34;data&#47;defaults.yaml&#34;</code> | |
| [environment_dns_zone](variables.tf#L44) | DNS zone suffix for environment. | <code>string</code> | | <code>null</code> | <code>2-networking</code> |
| [host_project_ids](variables.tf#L51) | Host project for the shared VPC. | <code title="object&#40;&#123;&#10; dev-spoke-0 &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> | <code>2-networking</code> |
| [vpc_self_links](variables.tf#L71) | Self link for the shared VPC. | <code title="object&#40;&#123;&#10; dev-spoke-0 &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> | <code>2-networking</code> |
| [factory_data](variables.tf#L32) | Project data from either YAML files or externally parsed data. | <code title="object&#40;&#123;&#10; data &#61; optional&#40;map&#40;any&#41;&#41;&#10; data_path &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | |
| [prefix](variables.tf#L48) | Prefix used for resources that need unique names. Use 9 characters or less. | <code>string</code> | ✓ | | <code>0-bootstrap</code> |
## Outputs
| name | description | sensitive | consumers |
|---|---|:---:|---|
| [projects](outputs.tf#L17) | Created projects and service accounts. | | |
| [projects](outputs.tf#L17) | Created projects. | | |
| [service_accounts](outputs.tf#L22) | Created service accounts. | | |
<!-- END TFDOC -->

View File

@ -16,43 +16,24 @@
# tfdoc:file:description Project factory.
locals {
_defaults = yamldecode(file(var.defaults_file))
_defaults_net = {
billing_account_id = var.billing_account.id
environment_dns_zone = var.environment_dns_zone
shared_vpc_self_link = try(var.vpc_self_links["dev-spoke-0"], null)
vpc_host_project = try(var.host_project_ids["dev-spoke-0"], null)
}
defaults = merge(local._defaults, local._defaults_net)
projects = {
for f in fileset("${var.data_dir}", "**/*.yaml") :
trimsuffix(f, ".yaml") => yamldecode(file("${var.data_dir}/${f}"))
}
}
module "projects" {
source = "../../../../blueprints/factories/project-factory"
for_each = local.projects
defaults = local.defaults
project_id = each.key
billing_account_id = try(each.value.billing_account_id, null)
billing_alert = try(each.value.billing_alert, null)
dns_zones = try(each.value.dns_zones, [])
essential_contacts = try(each.value.essential_contacts, [])
folder_id = try(each.value.folder_id, local.defaults.folder_id)
group_iam = try(each.value.group_iam, {})
iam = try(each.value.iam, {})
kms_service_agents = try(each.value.kms_service_agents, {})
labels = try(each.value.labels, {})
org_policies = try(each.value.org_policies, null)
prefix = var.prefix
service_accounts = try(each.value.service_accounts, {})
service_accounts_iam = try(each.value.service_accounts_iam, {})
services = try(each.value.services, [])
service_identities_iam = try(each.value.service_identities_iam, {})
vpc = try(each.value.vpc, null)
source = "../../../../blueprints/factories/project-factory"
data_defaults = {
billing_account = var.billing_account.id
# more defaults are available, check the project factory variables
}
data_merges = {
labels = {
environment = "dev"
}
services = [
"stackdriver.googleapis.com"
]
}
data_overrides = {
prefix = var.prefix
}
factory_data = var.factory_data
}

View File

@ -15,6 +15,11 @@
*/
output "projects" {
description = "Created projects and service accounts."
value = module.projects
description = "Created projects."
value = module.projects.projects
}
output "service_accounts" {
description = "Created service accounts."
value = module.projects.service_accounts
}

View File

@ -29,32 +29,20 @@ variable "billing_account" {
}
}
variable "data_dir" {
description = "Relative path for the folder storing configuration data."
type = string
default = "data/projects"
}
variable "defaults_file" {
description = "Relative path for the file storing the project factory configuration."
type = string
default = "data/defaults.yaml"
}
variable "environment_dns_zone" {
# tfdoc:variable:source 2-networking
description = "DNS zone suffix for environment."
type = string
default = null
}
variable "host_project_ids" {
# tfdoc:variable:source 2-networking
description = "Host project for the shared VPC."
variable "factory_data" {
description = "Project data from either YAML files or externally parsed data."
type = object({
dev-spoke-0 = string
data = optional(map(any))
data_path = optional(string)
})
default = null
nullable = false
validation {
condition = (
(var.factory_data.data != null ? 1 : 0) +
(var.factory_data.data_path != null ? 1 : 0)
) == 1
error_message = "One of data or data_path needs to be set."
}
}
variable "prefix" {
@ -67,12 +55,3 @@ variable "prefix" {
error_message = "Use a maximum of 9 characters for prefix."
}
}
variable "vpc_self_links" {
# tfdoc:variable:source 2-networking
description = "Self link for the shared VPC."
type = object({
dev-spoke-0 = string
})
default = null
}

View File

@ -0,0 +1,148 @@
# Refactor IAM interface
**authors:** [Ludo](https://github.com/ludoo), [Julio](https://github.com/juliocc)
**last modified:** August 17, 2023
## Status
Discussed.
## Context
Our modules IAM interface has evolved organically to progressively support more functionality, resulting in a large variable surface, lack of support for some key features like conditions, and some fragility for specific use cases.
We currently support, with uneven coverage across modules:
- authoritative `iam` in `ROLE => [PRINCIPALS]` format
- authoritative `group_iam` in `GROUP => [ROLES]` format
- legacy additive `iam_additive` in `ROLE => [PRINCIPALS]` format which breaks for dynamic values
- legacy additive `iam_additive_members` in `PRINCIPAL => [ROLES]` format which breaks for dynamic values
- new additive `iam_members` in `KEY => {role: ROLE, member: MEMBER, condition: CONDITION}` format which works with dynamic values and supports conditions
- policy authoritative `iam_policy`
- specific support for third party resource bindings in the service account module
## Proposal
### Authoritative bindings
These tend to work well in practice, and the current `iam` and `group_iam` variables are simple to use with good coverage across modules.
The only small use case that they do not cover is IAM conditions, which are easy to implement but would render the interface more verbose for the majority of cases where conditions are not needed.
The **proposal** for authoritative bindings is to
- leave the current interface in place (`iam` and `group_iam`)
- expand coverage so that all modules who have iam resources expose both
- add a new `iam_bindings` variable to support authoritative IAM with conditions
The new `iam_bindings` variable will look like this:
```hcl
variable "iam_bindings" {
description = "Authoritative IAM bindings with support for conditions, in {ROLE => { members = [], condition = {}}} format."
type = map(object({
members = list(string)
condition = optional(object({
expression = string
title = string
description = optional(string)
}))
}))
}
```
This variable will not be internally merged in modules with `iam` or `group_iam`.
### Additive bindings
Additive bindings have evolved to mimick authoritative ones, but the result is an interface which is bloated (no one uses `iam_additive_members`), and hard to understand and use without triggering dynamic errors. Coverage is also spotty and uneven across modules, and the interface needs to support aliasing of project service accounts in the project module to work around dynamic errors.
The `iam_additive` variable is used in a special patterns in data blueprints, to allow code to not mess up existing IAM bindings in an external project on destroy. This pattern only works in a limited set of cases, where principals are passed in via static variables or refer to "magic" static outputs in our modules. This is a simple example of the pattern:
```hcl
locals {
iam = {
"roles/viewer" = [
module.sa.iam_email,
var.group.admins
]
}
}
module "project" {
iam = (
var.project_create == null ? {} : local.iam
)
iam_additive = (
var.project_create != null ? {} : local.iam
)
}
```
The **proposal** for authoritative bindings is to
- remove `iam_additive` and `iam_additive_members` from the interface
- add a new `iam_bindings_additive` variable
Once new variables are in place, migrate existing blueprints to using `iam_bindings_additive` using one of the two available patterns:
- the flat verbose one where bindings are declared in the module call
- the more complex one that moves roles out to `locals` and uses them in `for` loops
The new variable will closely follow the type of the authoritative `iam_bindings` variable described above:
```hcl
variable "iam_bindings_additive" {
description = "Additive IAM bindings with support for conditions, in {KEY => { role = ROLE, members = [], condition = {}}} format."
type = map(object({
member = string
role = string
condition = optional(object({
expression = string
title = string
description = optional(string)
}))
}))
}
```
### IAM policy
The **proposal** is to remove the IAM policy variable and resources, as its coverage is very uneven and we never used it in practice. This will also simplify data access log management, which is currently split between its own variable/resource and the IAM policy ones.
## Decision
The proposal above summarizes the state of discussions between the authors, and implementation will be tested.
## Consequences
A few data blueprints that leverage `iam_additive` will need to be refactored to use the new variable, using one of the following patterns:
```hcl
locals {
network_sa_roles = [
"roles/compute.orgFirewallPolicyAdmin",
"roles/compute.xpnAdmin"
]
}
module "organization" {
source = "../../../modules/organization"
organization_id = "organizations/${var.organization.id}"
iam_bindings_additive = merge(
# IAM bindings via locals pattern
{
for r in local.network_sa_roles : "network_sa-${r}" : {
member = module.branch-network-sa.iam_email
role = r
}
},
# IAM bindings via explicit reference pattern
{
security_sa = {
member = module.branch-security-sa.iam_email
role = "roles/accesscontextmanager.policyAdmin"
}
}
)
}
```

3
modules/__docs/README.md Normal file
View File

@ -0,0 +1,3 @@
# FAST architectural documents
This folder contains assorted bits of documentation used to log current architectural choices, or past decisions. Format is inspired by [Michael Nygard's decision record template](https://github.com/joelparkerhenderson/architecture-decision-record/blob/main/templates/decision-record-template-by-michael-nygard/index.md).

View File

@ -251,6 +251,7 @@ module "cloud_run" {
By default `Compute default service account` is used to trigger Cloud Run. If you want to use custom Service Account you can either provide your own in `eventarc_triggers.service_account_email` or set `eventarc_triggers.service_account_create` to true and service account named `tf-cr-trigger-${var.name}` will be created with `roles/run.invoker` granted on this Cloud Run service.
Example using provided service account:
```hcl
module "cloud_run" {
source = "./fabric/modules/cloud-run"
@ -275,6 +276,7 @@ module "cloud_run" {
```
Example using automatically created service account:
```hcl
module "cloud_run" {
source = "./fabric/modules/cloud-run"
@ -296,7 +298,6 @@ module "cloud_run" {
# tftest modules=1 resources=5 inventory=trigger-service-account.yaml
```
### Service account
To use a custom service account managed by the module, set `service_account_create` to `true` and leave `service_account` set to `null` value (default).

View File

@ -5,6 +5,7 @@ This module simplifies the creation of [Data Catalog](https://cloud.google.com/d
Note: Data Catalog is still in beta, hence this module currently uses the beta provider.
<!-- BEGIN TOC -->
- [IAM](#iam)
- [Examples](#examples)
- [Simple Taxonomy with policy tags](#simple-taxonomy-with-policy-tags)
- [Taxonomy with IAM binding](#taxonomy-with-iam-binding)
@ -13,6 +14,18 @@ Note: Data Catalog is still in beta, hence this module currently uses the beta p
- [TODO](#todo)
<!-- END TOC -->
## IAM
IAM is managed via several variables that implement different features and levels of control:
- `iam` and `group_iam` configure authoritative bindings that manage individual roles exclusively, and are internally merged
- `iam_bindings` configure authoritative bindings with optional support for conditions, and are not internally merged with the previous two variables
- `iam_bindings_additive` configure additive bindings via individual role/member pairs with optional support conditions
The authoritative and additive approaches can be used together, provided different roles are managed by each. Some care must also be taken with the `groups_iam` variable to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
Refer to the [project module](../project/README.md#iam) for examples of the IAM interface.
## Examples
### Simple Taxonomy with policy tags
@ -52,7 +65,7 @@ module "cmn-dc" {
iam = {
"roles/datacatalog.categoryAdmin" = ["group:GROUP_NAME@example.com"]
}
iam_members = {
iam_bindings_additive = {
am1-admin = {
member = "user:am1@example.com"
role = "roles/datacatalog.categoryAdmin"
@ -66,18 +79,17 @@ module "cmn-dc" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [name](variables.tf#L74) | Name of this taxonomy. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L89) | GCP project id. | <code></code> | ✓ | |
| [name](variables.tf#L76) | Name of this taxonomy. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L91) | GCP project id. | <code></code> | ✓ | |
| [activated_policy_types](variables.tf#L17) | A list of policy types that are activated for this taxonomy. | <code>list&#40;string&#41;</code> | | <code>&#91;&#34;FINE_GRAINED_ACCESS_CONTROL&#34;&#93;</code> |
| [description](variables.tf#L23) | Description of this taxonomy. | <code>string</code> | | <code>&#34;Taxonomy - Terraform managed&#34;</code> |
| [group_iam](variables.tf#L29) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam](variables.tf#L35) | IAM bindings in {ROLE => [MEMBERS]} format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_additive](variables.tf#L41) | IAM additive bindings in {ROLE => [MEMBERS]} format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_additive_members](variables.tf#L47) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_members](variables.tf#L53) | Individual additive IAM bindings, use this when iam_additive does not work due to dynamic resources. Keys are arbitrary and only used for the internal loop. | <code title="map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [location](variables.tf#L68) | Data Catalog Taxonomy location. | <code>string</code> | | <code>&#34;eu&#34;</code> |
| [prefix](variables.tf#L79) | Optional prefix used to generate project id and name. | <code>string</code> | | <code>null</code> |
| [tags](variables.tf#L93) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | <code title="map&#40;object&#40;&#123;&#10; description &#61; optional&#40;string&#41;&#10; iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_bindings](variables.tf#L41) | Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}. | <code title="map&#40;object&#40;&#123;&#10; members &#61; list&#40;string&#41;&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_bindings_additive](variables.tf#L55) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [location](variables.tf#L70) | Data Catalog Taxonomy location. | <code>string</code> | | <code>&#34;eu&#34;</code> |
| [prefix](variables.tf#L81) | Optional prefix used to generate project id and name. | <code>string</code> | | <code>null</code> |
| [tags](variables.tf#L95) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | <code title="map&#40;object&#40;&#123;&#10; description &#61; optional&#40;string&#41;&#10; iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
## Outputs

View File

@ -23,16 +23,6 @@ locals {
]
}
_group_iam_roles = distinct(flatten(values(var.group_iam)))
_iam_additive_member_pairs = flatten([
for member, roles in var.iam_additive_members : [
for role in roles : { role = role, member = member }
]
])
_iam_additive_pairs = flatten([
for role, members in var.iam_additive : [
for member in members : { role = role, member = member }
]
])
iam = {
for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
role => concat(
@ -40,10 +30,6 @@ locals {
try(local._group_iam[role], [])
)
}
iam_additive = {
for pair in concat(local._iam_additive_pairs, local._iam_additive_member_pairs) :
"${pair.role}-${pair.member}" => pair
}
tags_iam = flatten([
for k, v in var.tags : [
for role, members in v.iam : {
@ -63,20 +49,25 @@ resource "google_data_catalog_taxonomy_iam_binding" "authoritative" {
members = each.value
}
resource "google_data_catalog_taxonomy_iam_member" "additive" {
resource "google_data_catalog_taxonomy_iam_binding" "bindings" {
provider = google-beta
for_each = (
length(var.iam_additive) + length(var.iam_additive_members) > 0
? local.iam_additive
: {}
)
for_each = var.iam_bindings
taxonomy = google_data_catalog_taxonomy.default.id
role = each.value.role
member = each.value.member
role = each.key
members = each.value.members
dynamic "condition" {
for_each = each.value.condition == null ? [] : [""]
content {
expression = each.value.condition.expression
title = each.value.condition.title
description = each.value.condition.description
}
}
}
resource "google_data_catalog_taxonomy_iam_member" "members" {
for_each = var.iam_members
resource "google_data_catalog_taxonomy_iam_member" "bindings" {
provider = google-beta
for_each = var.iam_bindings_additive
taxonomy = google_data_catalog_taxonomy.default.id
role = each.value.role
member = each.value.member

View File

@ -38,20 +38,22 @@ variable "iam" {
default = {}
}
variable "iam_additive" {
description = "IAM additive bindings in {ROLE => [MEMBERS]} format."
type = map(list(string))
default = {}
variable "iam_bindings" {
description = "Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}."
type = map(object({
members = list(string)
condition = optional(object({
expression = string
title = string
description = optional(string)
}))
}))
nullable = false
default = {}
}
variable "iam_additive_members" {
description = "IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values."
type = map(list(string))
default = {}
}
variable "iam_members" {
description = "Individual additive IAM bindings, use this when iam_additive does not work due to dynamic resources. Keys are arbitrary and only used for the internal loop."
variable "iam_bindings_additive" {
description = "Individual additive IAM bindings. Keys are arbitrary."
type = map(object({
member = string
role = string

View File

@ -316,8 +316,8 @@ The input variable 'data' is required to create a DataScan. This value is immuta
The input variable 'data' should be an object containing a single key-value pair that can be one of:
* `entity`: The Dataplex entity that represents the data source (e.g. BigQuery table) for DataScan, of the form: `projects/{project_number}/locations/{locationId}/lakes/{lakeId}/zones/{zoneId}/entities/{entityId}`.
* `resource`: The service-qualified full resource name of the cloud resource for a DataScan job to scan against. The field could be: BigQuery table of type "TABLE" for DataProfileScan/DataQualityScan format, e.g: `//bigquery.googleapis.com/projects/PROJECT_ID/datasets/DATASET_ID/tables/TABLE_ID`.
- `entity`: The Dataplex entity that represents the data source (e.g. BigQuery table) for DataScan, of the form: `projects/{project_number}/locations/{locationId}/lakes/{lakeId}/zones/{zoneId}/entities/{entityId}`.
- `resource`: The service-qualified full resource name of the cloud resource for a DataScan job to scan against. The field could be: BigQuery table of type "TABLE" for DataProfileScan/DataQualityScan format, e.g: `//bigquery.googleapis.com/projects/PROJECT_ID/datasets/DATASET_ID/tables/TABLE_ID`.
The example below shows how to specify the data source for DataScan of type `resource`:
@ -380,17 +380,15 @@ module "dataplex-datascan" {
## IAM
IAM is managed via several variables that implement different levels of control:
IAM is managed via several variables that implement different features and levels of control:
* `group_iam` and `iam` configure authoritative bindings that manage individual roles exclusively, mapping to the [`google_project_iam_binding`](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_iam#google_project_iam_binding) resource
* `iam_additive`, `iam_additive_members` and `iam_members` configure additive bindings that only manage individual role/member pairs, mapping to the [`google_project_iam_member`](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_iam#google_project_iam_member) resource
* `iam_policy` which controls the entire IAM policy for the project, where any binding created outside this module (eg in the console) will be removed at each `terraform apply` cycle regardless of the role
- `iam` and `group_iam` configure authoritative bindings that manage individual roles exclusively, and are internally merged
- `iam_bindings` configure authoritative bindings with optional support for conditions, and are not internally merged with the previous two variables
- `iam_bindings_additive` configure additive bindings via individual role/member pairs with optional support conditions
The authoritative and additive approaches can be used together, provided different roles are managed by each. The IAM policy is incompatible with the other approaches, and must be used with extreme care.
The authoritative and additive approaches can be used together, provided different roles are managed by each. Some care must also be taken with the `groups_iam` variable to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
Some care must also be taken with the `group_iam` and `iam_additive_*` variables to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph. For additive roles `iam_members` ensures that no dynamic values are used in the internal loop.
An example is provided below for using some of these variables.
An example is provided below for using some of these variables. Refer to the [project module](../project/README.md#iam) for complete examples of the IAM interface.
```hcl
module "dataplex-datascan" {
@ -416,7 +414,7 @@ module "dataplex-datascan" {
"roles/dataplex.dataScanViewer"
]
}
iam_members = {
iam_bindings_additive = {
am1-viewer = {
member = "user:am1@example.com"
role = "roles/dataplex.dataScanViewer"
@ -433,9 +431,9 @@ module "dataplex-datascan" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [data](variables.tf#L17) | The data source for DataScan. The source can be either a Dataplex `entity` or a BigQuery `resource`. | <code title="object&#40;&#123;&#10; entity &#61; optional&#40;string&#41;&#10; resource &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [name](variables.tf#L161) | Name of Dataplex Scan. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L172) | The ID of the project where the Dataplex DataScan will be created. | <code>string</code> | ✓ | |
| [region](variables.tf#L177) | Region for the Dataplex DataScan. | <code>string</code> | ✓ | |
| [name](variables.tf#L156) | Name of Dataplex Scan. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L167) | The ID of the project where the Dataplex DataScan will be created. | <code>string</code> | ✓ | |
| [region](variables.tf#L172) | Region for the Dataplex DataScan. | <code>string</code> | ✓ | |
| [data_profile_spec](variables.tf#L29) | DataProfileScan related setting. Variable descriptions are provided in https://cloud.google.com/dataplex/docs/reference/rest/v1/DataProfileSpec. | <code title="object&#40;&#123;&#10; sampling_percent &#61; optional&#40;number&#41;&#10; row_filter &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [data_quality_spec](variables.tf#L38) | DataQualityScan related setting. Variable descriptions are provided in https://cloud.google.com/dataplex/docs/reference/rest/v1/DataQualitySpec. | <code title="object&#40;&#123;&#10; sampling_percent &#61; optional&#40;number&#41;&#10; row_filter &#61; optional&#40;string&#41;&#10; rules &#61; list&#40;object&#40;&#123;&#10; column &#61; optional&#40;string&#41;&#10; ignore_null &#61; optional&#40;bool, null&#41;&#10; dimension &#61; string&#10; threshold &#61; optional&#40;number&#41;&#10; non_null_expectation &#61; optional&#40;object&#40;&#123;&#125;&#41;&#41;&#10; range_expectation &#61; optional&#40;object&#40;&#123;&#10; min_value &#61; optional&#40;number&#41;&#10; max_value &#61; optional&#40;number&#41;&#10; strict_min_enabled &#61; optional&#40;bool&#41;&#10; strict_max_enabled &#61; optional&#40;bool&#41;&#10; &#125;&#41;&#41;&#10; regex_expectation &#61; optional&#40;object&#40;&#123;&#10; regex &#61; string&#10; &#125;&#41;&#41;&#10; set_expectation &#61; optional&#40;object&#40;&#123;&#10; values &#61; list&#40;string&#41;&#10; &#125;&#41;&#41;&#10; uniqueness_expectation &#61; optional&#40;object&#40;&#123;&#125;&#41;&#41;&#10; statistic_range_expectation &#61; optional&#40;object&#40;&#123;&#10; statistic &#61; string&#10; min_value &#61; optional&#40;number&#41;&#10; max_value &#61; optional&#40;number&#41;&#10; strict_min_enabled &#61; optional&#40;bool&#41;&#10; strict_max_enabled &#61; optional&#40;bool&#41;&#10; &#125;&#41;&#41;&#10; row_condition_expectation &#61; optional&#40;object&#40;&#123;&#10; sql_expression &#61; string&#10; &#125;&#41;&#41;&#10; table_condition_expectation &#61; optional&#40;object&#40;&#123;&#10; sql_expression &#61; string&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [data_quality_spec_file](variables.tf#L80) | Path to a YAML file containing DataQualityScan related setting. Input content can use either camelCase or snake_case. Variables description are provided in https://cloud.google.com/dataplex/docs/reference/rest/v1/DataQualitySpec. | <code title="object&#40;&#123;&#10; path &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
@ -443,13 +441,11 @@ module "dataplex-datascan" {
| [execution_schedule](variables.tf#L94) | Schedule DataScan to run periodically based on a cron schedule expression. If not specified, the DataScan is created with `on_demand` schedule, which means it will not run until the user calls `dataScans.run` API. | <code>string</code> | | <code>null</code> |
| [group_iam](variables.tf#L100) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam](variables.tf#L107) | Dataplex DataScan IAM bindings in {ROLE => [MEMBERS]} format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_additive](variables.tf#L114) | IAM additive bindings in {ROLE => [MEMBERS]} format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_additive_members](variables.tf#L121) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_members](variables.tf#L127) | Individual additive IAM bindings, use this when iam_additive does not work due to dynamic resources. Keys are arbitrary and only used for the internal loop. | <code title="map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_policy](variables.tf#L142) | IAM authoritative policy in {ROLE => [MEMBERS]} format. Roles and members not explicitly listed will be cleared, use with extreme caution. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>null</code> |
| [incremental_field](variables.tf#L148) | The unnested field (of type Date or Timestamp) that contains values which monotonically increase over time. If not specified, a data scan will run for all data in the table. | <code>string</code> | | <code>null</code> |
| [labels](variables.tf#L154) | Resource labels. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [prefix](variables.tf#L166) | Optional prefix used to generate Dataplex DataScan ID. | <code>string</code> | | <code>null</code> |
| [iam_bindings](variables.tf#L114) | Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}. | <code title="map&#40;object&#40;&#123;&#10; members &#61; list&#40;string&#41;&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_bindings_additive](variables.tf#L128) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [incremental_field](variables.tf#L143) | The unnested field (of type Date or Timestamp) that contains values which monotonically increase over time. If not specified, a data scan will run for all data in the table. | <code>string</code> | | <code>null</code> |
| [labels](variables.tf#L149) | Resource labels. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [prefix](variables.tf#L161) | Optional prefix used to generate Dataplex DataScan ID. | <code>string</code> | | <code>null</code> |
## Outputs

View File

@ -21,16 +21,6 @@ locals {
for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
]
}
_iam_additive_pairs = flatten([
for role, members in var.iam_additive : [
for member in members : { role = role, member = member }
]
])
_iam_additive_member_pairs = flatten([
for member, roles in var.iam_additive_members : [
for role in roles : { role = role, member = member }
]
])
iam = {
for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
role => concat(
@ -38,13 +28,6 @@ locals {
try(local._group_iam[role], [])
)
}
iam_additive = {
for pair in concat(local._iam_additive_pairs, local._iam_additive_member_pairs) :
"${pair.role}-${pair.member}" => {
role = pair.role
member = pair.member
}
}
}
resource "google_dataplex_datascan_iam_binding" "authoritative_for_role" {
@ -56,21 +39,25 @@ resource "google_dataplex_datascan_iam_binding" "authoritative_for_role" {
members = each.value
}
resource "google_dataplex_datascan_iam_member" "additive" {
for_each = (
length(var.iam_additive) + length(var.iam_additive_members) > 0
? local.iam_additive
: {}
)
resource "google_dataplex_datascan_iam_binding" "bindings" {
for_each = var.iam_bindings
project = google_dataplex_datascan.datascan.project
location = google_dataplex_datascan.datascan.location
data_scan_id = google_dataplex_datascan.datascan.data_scan_id
role = each.value.role
member = each.value.member
role = each.key
members = each.value.members
dynamic "condition" {
for_each = each.value.condition == null ? [] : [""]
content {
expression = each.value.condition.expression
title = each.value.condition.title
description = each.value.condition.description
}
}
}
resource "google_dataplex_datascan_iam_member" "members" {
for_each = var.iam_members
resource "google_dataplex_datascan_iam_member" "bindings" {
for_each = var.iam_bindings_additive
project = google_dataplex_datascan.datascan.project
location = google_dataplex_datascan.datascan.location
data_scan_id = google_dataplex_datascan.datascan.data_scan_id
@ -85,22 +72,3 @@ resource "google_dataplex_datascan_iam_member" "members" {
}
}
}
resource "google_dataplex_datascan_iam_policy" "authoritative_for_resource" {
count = var.iam_policy != null ? 1 : 0
project = google_dataplex_datascan.datascan.project
location = google_dataplex_datascan.datascan.location
data_scan_id = google_dataplex_datascan.datascan.data_scan_id
policy_data = data.google_iam_policy.authoritative.0.policy_data
}
data "google_iam_policy" "authoritative" {
count = var.iam_policy != null ? 1 : 0
dynamic "binding" {
for_each = try(var.iam_policy, {})
content {
role = binding.key
members = binding.value
}
}
}

View File

@ -111,21 +111,22 @@ variable "iam" {
nullable = false
}
variable "iam_additive" {
description = "IAM additive bindings in {ROLE => [MEMBERS]} format."
type = map(list(string))
default = {}
nullable = false
variable "iam_bindings" {
description = "Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}."
type = map(object({
members = list(string)
condition = optional(object({
expression = string
title = string
description = optional(string)
}))
}))
nullable = false
default = {}
}
variable "iam_additive_members" {
description = "IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values."
type = map(list(string))
default = {}
}
variable "iam_members" {
description = "Individual additive IAM bindings, use this when iam_additive does not work due to dynamic resources. Keys are arbitrary and only used for the internal loop."
variable "iam_bindings_additive" {
description = "Individual additive IAM bindings. Keys are arbitrary."
type = map(object({
member = string
role = string
@ -139,12 +140,6 @@ variable "iam_members" {
default = {}
}
variable "iam_policy" {
description = "IAM authoritative policy in {ROLE => [MEMBERS]} format. Roles and members not explicitly listed will be cleared, use with extreme caution."
type = map(list(string))
default = null
}
variable "incremental_field" {
description = "The unnested field (of type Date or Timestamp) that contains values which monotonically increase over time. If not specified, a data scan will run for all data in the table."
type = string

File diff suppressed because one or more lines are too long

View File

@ -23,11 +23,6 @@ locals {
for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
]
}
_iam_additive_pairs = flatten([
for role, members in var.iam_additive : [
for member in members : { role = role, member = member }
]
])
iam = {
for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
role => concat(
@ -35,13 +30,6 @@ locals {
try(local._group_iam[role], [])
)
}
iam_additive = {
for pair in local._iam_additive_pairs :
"${pair.role}-${pair.member}" => {
role = pair.role
member = pair.member
}
}
}
resource "google_dataproc_cluster_iam_binding" "authoritative" {
@ -53,23 +41,28 @@ resource "google_dataproc_cluster_iam_binding" "authoritative" {
members = each.value
}
resource "google_dataproc_cluster_iam_member" "additive" {
for_each = (
length(var.iam_additive) > 0
? local.iam_additive
: {}
)
project = var.project_id
cluster = google_dataproc_cluster.cluster.name
region = var.region
role = each.value.role
member = each.value.member
}
resource "google_dataproc_cluster_iam_member" "members" {
for_each = var.iam_members
resource "google_dataproc_cluster_iam_binding" "bindings" {
for_each = var.iam_bindings
project = var.project_id
cluster = google_dataproc_cluster.cluster.name
region = var.region
role = each.key
members = each.value.members
dynamic "condition" {
for_each = each.value.condition == null ? [] : [""]
content {
expression = each.value.condition.expression
title = each.value.condition.title
description = each.value.condition.description
}
}
}
resource "google_dataproc_cluster_iam_member" "bindings" {
for_each = var.iam_bindings_additive
project = var.project_id
cluster = google_dataproc_cluster.cluster.name
region = var.region
role = each.value.role
member = each.value.member
dynamic "condition" {

View File

@ -196,15 +196,22 @@ variable "iam" {
nullable = false
}
variable "iam_additive" {
description = "IAM additive bindings in {ROLE => [MEMBERS]} format."
type = map(list(string))
default = {}
nullable = false
variable "iam_bindings" {
description = "Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}."
type = map(object({
members = list(string)
condition = optional(object({
expression = string
title = string
description = optional(string)
}))
}))
nullable = false
default = {}
}
variable "iam_members" {
description = "Individual additive IAM bindings, use this when iam_additive does not work due to dynamic resources. Keys are arbitrary and only used for the internal loop."
variable "iam_bindings_additive" {
description = "Individual additive IAM bindings. Keys are arbitrary."
type = map(object({
member = string
role = string

View File

@ -33,35 +33,27 @@ module "folder" {
iam = {
"roles/owner" = ["user:one@example.org"]
}
iam_additive = {
"roles/compute.admin" = ["user:a1@example.org", "user:a2@example.org"]
"roles/compute.viewer" = ["user:a2@example.org"]
}
iam_additive_members = {
"user:am1@example.org" = ["roles/storage.admin"]
"user:am2@example.org" = ["roles/storage.objectViewer"]
}
iam_members = {
iam_bindings_additive = {
am1-storage-admin = {
member = "user:am1@example.org"
role = "roles/storage.admin"
}
}
}
# tftest modules=1 resources=10 inventory=iam.yaml
# tftest modules=1 resources=5 inventory=iam.yaml
```
## IAM
There are four three exclusive ways at the role level of managing IAM in this module
IAM is managed via several variables that implement different features and levels of control:
- non-authoritative via the `iam_additive`, `iam_additive_members` and `iam_members` variables, where bindings created outside this module will coexist with those managed here
- authoritative via the `group_iam` and `iam` variables, where bindings created outside this module (eg in the console) will be removed at each `terraform apply` cycle if the same role is also managed here
- authoritative policy via the `iam_policy` variable, where any binding created outside this module (eg in the console) will be removed at each `terraform apply` cycle regardless of the role
- `iam` and `group_iam` configure authoritative bindings that manage individual roles exclusively, and are internally merged
- `iam_bindings` configure authoritative bindings with optional support for conditions, and are not internally merged with the previous two variables
- `iam_bindings_additive` configure additive bindings via individual role/member pairs with optional support conditions
The authoritative and additive approaches can be used together, provided different roles are managed by each. The IAM policy is incompatible with the other approaches, and must be used with extreme care.
The authoritative and additive approaches can be used together, provided different roles are managed by each. Some care must also be taken with the `groups_iam` variable to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
Some care must be taken with the `groups_iam` variable (and in some situations with the additive variables) to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
Refer to the [project module](../project/README.md#iam) for examples of the IAM interface.
## Organization policies
@ -241,32 +233,6 @@ module "folder" {
# tftest modules=1 resources=3 inventory=logging-data-access.yaml
```
While this sets an authoritative policies that has exclusive control of both IAM bindings for all roles and data access log configuration, and should be used with extreme care:
```hcl
module "folder" {
source = "./fabric/modules/folder"
parent = "folders/657104291943"
name = "my-folder"
iam_policy = {
"roles/owner" = ["group:org-admins@example.com"]
"roles/resourcemanager.folderAdmin" = ["group:org-admins@example.com"]
"roles/resourcemanager.organizationAdmin" = ["group:org-admins@example.com"]
"roles/resourcemanager.projectCreator" = ["group:org-admins@example.com"]
}
logging_data_access = {
allServices = {
ADMIN_READ = ["group:organization-admins@example.org"]
}
"storage.googleapis.com" = {
DATA_READ = []
DATA_WRITE = []
}
}
}
# tftest modules=1 resources=2 inventory=iam-policy.yaml
```
## Tags
Refer to the [Creating and managing tags](https://cloud.google.com/resource-manager/docs/tags/tags-creating-and-managing) documentation for details on usage.
@ -305,7 +271,7 @@ module "folder" {
| name | description | resources |
|---|---|---|
| [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | <code>google_folder_iam_binding</code> · <code>google_folder_iam_member</code> · <code>google_folder_iam_policy</code> |
| [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | <code>google_folder_iam_binding</code> · <code>google_folder_iam_member</code> |
| [logging.tf](./logging.tf) | Log sinks and supporting resources. | <code>google_bigquery_dataset_iam_member</code> · <code>google_folder_iam_audit_config</code> · <code>google_logging_folder_exclusion</code> · <code>google_logging_folder_sink</code> · <code>google_project_iam_member</code> · <code>google_pubsub_topic_iam_member</code> · <code>google_storage_bucket_iam_member</code> |
| [main.tf](./main.tf) | Module-level locals and resources. | <code>google_compute_firewall_policy_association</code> · <code>google_essential_contacts_contact</code> · <code>google_folder</code> |
| [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | <code>google_org_policy_policy</code> |
@ -323,19 +289,17 @@ module "folder" {
| [folder_create](variables.tf#L31) | Create folder. When set to false, uses id to reference an existing folder. | <code>bool</code> | | <code>true</code> |
| [group_iam](variables.tf#L37) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam](variables.tf#L44) | IAM bindings in {ROLE => [MEMBERS]} format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_additive](variables.tf#L51) | Non authoritative IAM bindings, in {ROLE => [MEMBERS]} format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_additive_members](variables.tf#L58) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_members](variables.tf#L65) | Individual additive IAM bindings, use this when iam_additive does not work due to dynamic resources. Keys are arbitrary and only used for the internal loop. | <code title="map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_policy](variables.tf#L80) | IAM authoritative policy in {ROLE => [MEMBERS]} format. Roles and members not explicitly listed will be cleared, use with extreme caution. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>null</code> |
| [id](variables.tf#L86) | Folder ID in case you use folder_create=false. | <code>string</code> | | <code>null</code> |
| [logging_data_access](variables.tf#L92) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | <code>map&#40;map&#40;list&#40;string&#41;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [logging_exclusions](variables.tf#L107) | Logging exclusions for this folder in the form {NAME -> FILTER}. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [logging_sinks](variables.tf#L114) | Logging sinks to create for the organization. | <code title="map&#40;object&#40;&#123;&#10; bq_partitioned_table &#61; optional&#40;bool&#41;&#10; description &#61; optional&#40;string&#41;&#10; destination &#61; string&#10; disabled &#61; optional&#40;bool, false&#41;&#10; exclusions &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; filter &#61; string&#10; include_children &#61; optional&#40;bool, true&#41;&#10; type &#61; string&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [name](variables.tf#L144) | Folder name. | <code>string</code> | | <code>null</code> |
| [org_policies](variables.tf#L150) | Organization policies applied to this folder keyed by policy name. | <code title="map&#40;object&#40;&#123;&#10; inherit_from_parent &#61; optional&#40;bool&#41; &#35; for list policies only.&#10; reset &#61; optional&#40;bool&#41;&#10; rules &#61; optional&#40;list&#40;object&#40;&#123;&#10; allow &#61; optional&#40;object&#40;&#123;&#10; all &#61; optional&#40;bool&#41;&#10; values &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;&#10; deny &#61; optional&#40;object&#40;&#123;&#10; all &#61; optional&#40;bool&#41;&#10; values &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;&#10; enforce &#61; optional&#40;bool&#41; &#35; for boolean policies only.&#10; condition &#61; optional&#40;object&#40;&#123;&#10; description &#61; optional&#40;string&#41;&#10; expression &#61; optional&#40;string&#41;&#10; location &#61; optional&#40;string&#41;&#10; title &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [org_policies_data_path](variables.tf#L177) | Path containing org policies in YAML format. | <code>string</code> | | <code>null</code> |
| [parent](variables.tf#L183) | Parent in folders/folder_id or organizations/org_id format. | <code>string</code> | | <code>null</code> |
| [tag_bindings](variables.tf#L193) | Tag bindings for this folder, in key => tag value id format. | <code>map&#40;string&#41;</code> | | <code>null</code> |
| [iam_bindings](variables.tf#L51) | Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}. | <code title="map&#40;object&#40;&#123;&#10; members &#61; list&#40;string&#41;&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_bindings_additive](variables.tf#L65) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [id](variables.tf#L80) | Folder ID in case you use folder_create=false. | <code>string</code> | | <code>null</code> |
| [logging_data_access](variables.tf#L86) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | <code>map&#40;map&#40;list&#40;string&#41;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [logging_exclusions](variables.tf#L101) | Logging exclusions for this folder in the form {NAME -> FILTER}. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [logging_sinks](variables.tf#L108) | Logging sinks to create for the organization. | <code title="map&#40;object&#40;&#123;&#10; bq_partitioned_table &#61; optional&#40;bool&#41;&#10; description &#61; optional&#40;string&#41;&#10; destination &#61; string&#10; disabled &#61; optional&#40;bool, false&#41;&#10; exclusions &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; filter &#61; string&#10; include_children &#61; optional&#40;bool, true&#41;&#10; type &#61; string&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [name](variables.tf#L138) | Folder name. | <code>string</code> | | <code>null</code> |
| [org_policies](variables.tf#L144) | Organization policies applied to this folder keyed by policy name. | <code title="map&#40;object&#40;&#123;&#10; inherit_from_parent &#61; optional&#40;bool&#41; &#35; for list policies only.&#10; reset &#61; optional&#40;bool&#41;&#10; rules &#61; optional&#40;list&#40;object&#40;&#123;&#10; allow &#61; optional&#40;object&#40;&#123;&#10; all &#61; optional&#40;bool&#41;&#10; values &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;&#10; deny &#61; optional&#40;object&#40;&#123;&#10; all &#61; optional&#40;bool&#41;&#10; values &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;&#10; enforce &#61; optional&#40;bool&#41; &#35; for boolean policies only.&#10; condition &#61; optional&#40;object&#40;&#123;&#10; description &#61; optional&#40;string&#41;&#10; expression &#61; optional&#40;string&#41;&#10; location &#61; optional&#40;string&#41;&#10; title &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [org_policies_data_path](variables.tf#L171) | Path containing org policies in YAML format. | <code>string</code> | | <code>null</code> |
| [parent](variables.tf#L177) | Parent in folders/folder_id or organizations/org_id format. | <code>string</code> | | <code>null</code> |
| [tag_bindings](variables.tf#L187) | Tag bindings for this folder, in key => tag value id format. | <code>map&#40;string&#41;</code> | | <code>null</code> |
## Outputs

View File

@ -23,16 +23,6 @@ locals {
for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
]
}
_iam_additive_pairs = flatten([
for role, members in var.iam_additive : [
for member in members : { role = role, member = member }
]
])
_iam_additive_member_pairs = flatten([
for member, roles in var.iam_additive_members : [
for role in roles : { role = role, member = member }
]
])
iam = {
for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
role => concat(
@ -40,10 +30,6 @@ locals {
try(local._group_iam[role], [])
)
}
iam_additive = {
for pair in concat(local._iam_additive_pairs, local._iam_additive_member_pairs) :
"${pair.role}-${pair.member}" => pair
}
}
resource "google_folder_iam_binding" "authoritative" {
@ -53,19 +39,23 @@ resource "google_folder_iam_binding" "authoritative" {
members = each.value
}
resource "google_folder_iam_member" "additive" {
for_each = (
length(var.iam_additive) + length(var.iam_additive_members) > 0
? local.iam_additive
: {}
)
folder = local.folder.name
role = each.value.role
member = each.value.member
resource "google_folder_iam_binding" "bindings" {
for_each = var.iam_bindings
folder = local.folder.name
role = each.key
members = each.value.members
dynamic "condition" {
for_each = each.value.condition == null ? [] : [""]
content {
expression = each.value.condition.expression
title = each.value.condition.title
description = each.value.condition.description
}
}
}
resource "google_folder_iam_member" "members" {
for_each = var.iam_members
resource "google_folder_iam_member" "bindings" {
for_each = var.iam_bindings_additive
folder = local.folder.name
role = each.value.role
member = each.value.member
@ -78,34 +68,3 @@ resource "google_folder_iam_member" "members" {
}
}
}
resource "google_folder_iam_policy" "authoritative" {
count = var.iam_policy != null ? 1 : 0
folder = local.folder.name
policy_data = data.google_iam_policy.authoritative.0.policy_data
}
data "google_iam_policy" "authoritative" {
count = var.iam_policy != null ? 1 : 0
dynamic "binding" {
for_each = try(var.iam_policy, {})
content {
role = binding.key
members = binding.value
}
}
dynamic "audit_config" {
for_each = var.logging_data_access
content {
service = audit_config.key
dynamic "audit_log_configs" {
for_each = audit_config.value
iterator = config
content {
log_type = config.key
exempted_members = config.value
}
}
}
}
}

View File

@ -28,11 +28,9 @@ locals {
}
resource "google_folder_iam_audit_config" "default" {
for_each = (
var.iam_policy == null ? var.logging_data_access : {}
)
folder = local.folder.name
service = each.key
for_each = var.logging_data_access
folder = local.folder.name
service = each.key
dynamic "audit_log_config" {
for_each = each.value
iterator = config

View File

@ -24,7 +24,7 @@ output "id" {
value = local.folder.name
depends_on = [
google_folder_iam_binding.authoritative,
google_folder_iam_policy.authoritative,
google_folder_iam_binding.bindings,
google_org_policy_policy.default,
]
}
@ -37,6 +37,7 @@ output "name" {
output "sink_writer_identities" {
description = "Writer identities created for each sink."
value = {
for name, sink in google_logging_folder_sink.sink : name => sink.writer_identity
for name, sink in google_logging_folder_sink.sink :
name => sink.writer_identity
}
}

Some files were not shown because too many files have changed in this diff Show More