From 3488d725ef892b6ed9b22443761701033d0277c5 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Wed, 31 Aug 2022 18:08:53 +0200 Subject: [PATCH 001/132] Added POC Example (#14) * Added Initial Actions Workflow * Adapted Dev Environment POC * Fixed Branch * Using Github Runners * Fixed Issue with too many commands * Testing Removal of Brackets * Removed Access Token * Fixed Artefact Saving & Changed to Github Hosted Runner for deployment * Removed Whitespace from Project Name * Enabling Secret Manager * Added cloudresourcemanager to dependancy. * Added WIP & SA with repository secrets example * Updated Dependancies * Testing with Basic TF * Added Project * Adapted Module * Changed Label * Added Stage Config * Removal of Vars from TF Apply * Updated to use Github Secrets * Adjusted Stage & Prod Workflows * Added Variable & Static Secret Example * Added Static & Variable Secrets Example * Added a TF depedancy * Added Stages to State Prefix * Fixed Config with Stage & Prod * Changed Secrets to Colors * DEV -> STAGE * STAGE -> PROD * Updated to Blue * Update main.tf * Update main.tf --- .github/workflows/tf-actions-dev.yml | 151 ++++++++++++++++++ .github/workflows/tf-actions-prod.yml | 151 ++++++++++++++++++ .github/workflows/tf-actions-stage.yml | 151 ++++++++++++++++++ examples/poc/config/dev.tfvars | 1 + examples/poc/config/prod.tfvars | 1 + examples/poc/config/stage.tfvars | 1 + examples/poc/main.tf | 37 +++++ examples/poc/modules/secret-manager/README.md | 3 + examples/poc/modules/secret-manager/main.tf | 46 ++++++ .../poc/modules/secret-manager/outputs.tf | 17 ++ .../poc/modules/secret-manager/variables.tf | 36 +++++ .../poc/modules/secret-manager/versions.tf | 29 ++++ examples/poc/provider.tf | 28 ++++ examples/poc/variables.tf | 30 ++++ 14 files changed, 682 insertions(+) create mode 100644 .github/workflows/tf-actions-dev.yml create mode 100644 .github/workflows/tf-actions-prod.yml create mode 100644 .github/workflows/tf-actions-stage.yml create mode 100644 examples/poc/config/dev.tfvars create mode 100644 examples/poc/config/prod.tfvars create mode 100644 examples/poc/config/stage.tfvars create mode 100644 examples/poc/main.tf create mode 100644 examples/poc/modules/secret-manager/README.md create mode 100644 examples/poc/modules/secret-manager/main.tf create mode 100644 examples/poc/modules/secret-manager/outputs.tf create mode 100644 examples/poc/modules/secret-manager/variables.tf create mode 100644 examples/poc/modules/secret-manager/versions.tf create mode 100644 examples/poc/provider.tf create mode 100644 examples/poc/variables.tf diff --git a/.github/workflows/tf-actions-dev.yml b/.github/workflows/tf-actions-dev.yml new file mode 100644 index 0000000..66ba78c --- /dev/null +++ b/.github/workflows/tf-actions-dev.yml @@ -0,0 +1,151 @@ +# 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 +# +# 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. + + +name: DEV Environment POC +on: + push: + branches: + - poc-dev +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.DEV_SERVICE_ACCOUNT }} + TF_VAR_project: ${{ secrets.DEV_PROJECT }} + TF_VAR_stage: 'dev' + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' +# TF_VAR_project: 'XXXXXXX' + +jobs: + # Validate + validate: + # The type of runner that the job will run on + runs-on: ubuntu-latest + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: 'actions/checkout@v2' + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - id: 'tfsetup' + name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + - run: |- + cd ./examples/poc/ + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY/$TF_VAR_stage" + terraform validate + + # Plan + plan: + needs: validate + # The type of runner that the job will run on + runs-on: ubuntu-latest + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: 'actions/checkout@v2' + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - id: 'tfsetup' + name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + - run: |- + cd ./examples/poc/ + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY/$TF_VAR_stage" + terraform plan -out tfplan.plan -var-file="config/dev.tfvars" + - name: 'Save Plan' + uses: actions/upload-artifact@v2 + with: + name: tfplan.plan + path: ./examples/poc/tfplan.plan + + # Deploy + deploy: + needs: plan + # The type of runner that the job will run on + runs-on: ubuntu-latest + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: 'actions/checkout@v2' + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - id: 'tfsetup' + name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + - uses: actions/download-artifact@v2 + with: + name: tfplan.plan + path: ./examples/poc/ + + - run: |- + cd ./examples/poc/ + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY/$TF_VAR_stage" + terraform apply -auto-approve tfplan.plan diff --git a/.github/workflows/tf-actions-prod.yml b/.github/workflows/tf-actions-prod.yml new file mode 100644 index 0000000..d1f02e3 --- /dev/null +++ b/.github/workflows/tf-actions-prod.yml @@ -0,0 +1,151 @@ +# 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 +# +# 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. + + +name: PROD Environment POC +on: + push: + branches: + - poc-prod +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.PROD_SERVICE_ACCOUNT }} + TF_VAR_project: ${{ secrets.PROD_PROJECT }} + TF_VAR_stage: 'prod' + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' +# TF_VAR_project: 'XXXXXXX' + +jobs: + # Validate + validate: + # The type of runner that the job will run on + runs-on: ubuntu-latest + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: 'actions/checkout@v2' + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - id: 'tfsetup' + name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + - run: |- + cd ./examples/poc/ + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY/$TF_VAR_stage" + terraform validate + + # Plan + plan: + needs: validate + # The type of runner that the job will run on + runs-on: ubuntu-latest + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: 'actions/checkout@v2' + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - id: 'tfsetup' + name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + - run: |- + cd ./examples/poc/ + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY/$TF_VAR_stage" + terraform plan -out tfplan.plan -var-file="config/prod.tfvars" + - name: 'Save Plan' + uses: actions/upload-artifact@v2 + with: + name: tfplan.plan + path: ./examples/poc/tfplan.plan + + # Deploy + deploy: + needs: plan + # The type of runner that the job will run on + runs-on: ubuntu-latest + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: 'actions/checkout@v2' + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - id: 'tfsetup' + name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + - uses: actions/download-artifact@v2 + with: + name: tfplan.plan + path: ./examples/poc/ + + - run: |- + cd ./examples/poc/ + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY/$TF_VAR_stage" + terraform apply -auto-approve tfplan.plan diff --git a/.github/workflows/tf-actions-stage.yml b/.github/workflows/tf-actions-stage.yml new file mode 100644 index 0000000..7357628 --- /dev/null +++ b/.github/workflows/tf-actions-stage.yml @@ -0,0 +1,151 @@ +# 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 +# +# 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. + + +name: SIT Environment POC +on: + push: + branches: + - poc-stage +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.STAGE_SERVICE_ACCOUNT }} + TF_VAR_project: ${{ secrets.STAGE_PROJECT }} + TF_VAR_stage: 'stage' + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' +# TF_VAR_project: 'XXXXXXX' + +jobs: + # Validate + validate: + # The type of runner that the job will run on + runs-on: ubuntu-latest + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: 'actions/checkout@v2' + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - id: 'tfsetup' + name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + - run: |- + cd ./examples/poc/ + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY/$TF_VAR_stage" + terraform validate + + # Plan + plan: + needs: validate + # The type of runner that the job will run on + runs-on: ubuntu-latest + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: 'actions/checkout@v2' + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - id: 'tfsetup' + name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + - run: |- + cd ./examples/poc/ + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY/$TF_VAR_stage" + terraform plan -out tfplan.plan -var-file="config/stage.tfvars" + - name: 'Save Plan' + uses: actions/upload-artifact@v2 + with: + name: tfplan.plan + path: ./examples/poc/tfplan.plan + + # Deploy + deploy: + needs: plan + # The type of runner that the job will run on + runs-on: ubuntu-latest + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: 'actions/checkout@v2' + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - id: 'tfsetup' + name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + - uses: actions/download-artifact@v2 + with: + name: tfplan.plan + path: ./examples/poc/ + + - run: |- + cd ./examples/poc/ + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY/$TF_VAR_stage" + terraform apply -auto-approve tfplan.plan diff --git a/examples/poc/config/dev.tfvars b/examples/poc/config/dev.tfvars new file mode 100644 index 0000000..cf95bed --- /dev/null +++ b/examples/poc/config/dev.tfvars @@ -0,0 +1 @@ +secret = "red" diff --git a/examples/poc/config/prod.tfvars b/examples/poc/config/prod.tfvars new file mode 100644 index 0000000..fc4227f --- /dev/null +++ b/examples/poc/config/prod.tfvars @@ -0,0 +1 @@ +secret = "blue" \ No newline at end of file diff --git a/examples/poc/config/stage.tfvars b/examples/poc/config/stage.tfvars new file mode 100644 index 0000000..6d861fe --- /dev/null +++ b/examples/poc/config/stage.tfvars @@ -0,0 +1 @@ +secret = "green" \ No newline at end of file diff --git a/examples/poc/main.tf b/examples/poc/main.tf new file mode 100644 index 0000000..e2ddf78 --- /dev/null +++ b/examples/poc/main.tf @@ -0,0 +1,37 @@ +/** + * 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 + * + * 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. + */ + +# This is an example of infrastructure does change over different stages/environments. +# The configuration is kept in the config folder and used to deploy to the relevant stage. +module "secret-manager-variable" { + source = "./modules/secret-manager" + label = "devdps-governance-secret-variable-over-stages" + project_id = var.project + secret_id = "dg-secret-variable" + secret_version = var.variable-secret +} + +# This is an example of infrastructure does not change over different stages/environments. +module "secret-manager-static" { + source = "./modules/secret-manager" + label = "devdps-governance-secret-static-over-stages" + project_id = var.project + secret_id = "dg-secret-static" + secret_version = "red" + depends_on = [module.secret-manager-variable] +} + + diff --git a/examples/poc/modules/secret-manager/README.md b/examples/poc/modules/secret-manager/README.md new file mode 100644 index 0000000..debfe0b --- /dev/null +++ b/examples/poc/modules/secret-manager/README.md @@ -0,0 +1,3 @@ +# Google Secret Manager Module + +Simple Secret Manager module that allows managing one secret and its versions. \ No newline at end of file diff --git a/examples/poc/modules/secret-manager/main.tf b/examples/poc/modules/secret-manager/main.tf new file mode 100644 index 0000000..3a92b3d --- /dev/null +++ b/examples/poc/modules/secret-manager/main.tf @@ -0,0 +1,46 @@ +/** + * 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 + * + * 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. + */ + +resource "google_project_service" "cloudresourcemanager" { + project = var.project_id + service = "cloudresourcemanager.googleapis.com" +} + +resource "google_project_service" "secret-manager" { + project = var.project_id + service = "secretmanager.googleapis.com" + depends_on = [google_project_service.cloudresourcemanager] +} + +resource "google_secret_manager_secret" "secret-basic" { + project = var.project_id + secret_id = var.secret_id + + labels = { + label = var.label + } + + replication { + automatic = true + } + depends_on = [google_project_service.secret-manager] +} + + +resource "google_secret_manager_secret_version" "secret-version-basic" { + secret = google_secret_manager_secret.secret-basic.id + secret_data = var.secret_version +} \ No newline at end of file diff --git a/examples/poc/modules/secret-manager/outputs.tf b/examples/poc/modules/secret-manager/outputs.tf new file mode 100644 index 0000000..b4ee623 --- /dev/null +++ b/examples/poc/modules/secret-manager/outputs.tf @@ -0,0 +1,17 @@ +/** + * 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 + * + * 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. + */ + + diff --git a/examples/poc/modules/secret-manager/variables.tf b/examples/poc/modules/secret-manager/variables.tf new file mode 100644 index 0000000..b6c44d3 --- /dev/null +++ b/examples/poc/modules/secret-manager/variables.tf @@ -0,0 +1,36 @@ +/** + * 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 + * + * 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. + */ + +variable "label" { + description = "A single label for the secret" + type = string +} + +variable "project_id" { + description = "Project id where the keyring will be created." + type = string +} + +variable "secret_id" { + description = "A single secret id to store" + type = string +} + + +variable "secret_version" { + description = "A single secret version to store" + type = string +} diff --git a/examples/poc/modules/secret-manager/versions.tf b/examples/poc/modules/secret-manager/versions.tf new file mode 100644 index 0000000..d271fb7 --- /dev/null +++ b/examples/poc/modules/secret-manager/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.1.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.25.0" # tftest + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.25.0" # tftest + } + } +} + + diff --git a/examples/poc/provider.tf b/examples/poc/provider.tf new file mode 100644 index 0000000..0802a09 --- /dev/null +++ b/examples/poc/provider.tf @@ -0,0 +1,28 @@ +/** + * 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 + * + * 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. + */ + +terraform { + backend "gcs" { + } +} + +provider "google" { + +} + +provider "google-beta" { + +} \ No newline at end of file diff --git a/examples/poc/variables.tf b/examples/poc/variables.tf new file mode 100644 index 0000000..d67629c --- /dev/null +++ b/examples/poc/variables.tf @@ -0,0 +1,30 @@ +/** + * 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 + * + * 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. + */ + +variable "project" { + type = string + default = "project-id" +} + +variable "stage" { + type = string + default = "dev" +} + +variable "variable-secret" { + type = string + default = "secret" +} \ No newline at end of file From 3ff64092711dde48ba9a5bf006b91125aa5b24ea Mon Sep 17 00:00:00 2001 From: Byron Whitlock Date: Wed, 30 Nov 2022 15:20:25 -0800 Subject: [PATCH 002/132] added sample workflows for other environments for GDC --- .../folder-factory/.cloudbuild/workflows/cloudbuild.yaml | 0 .../guardrails/folder-factory/.gitlab/workflows/cloudbuild.yaml | 0 .../guardrails/folder-factory/.jenkins/workflows/cloudbuild.yaml | 0 .../folder-factory/.terraform-cloud/workflows/cloudbuild.yaml | 0 .../folder-factory/.terraform-enterprise/worflows/cloudbuild.yaml | 0 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/guardrails/folder-factory/.cloudbuild/workflows/cloudbuild.yaml create mode 100644 examples/guardrails/folder-factory/.gitlab/workflows/cloudbuild.yaml create mode 100644 examples/guardrails/folder-factory/.jenkins/workflows/cloudbuild.yaml create mode 100644 examples/guardrails/folder-factory/.terraform-cloud/workflows/cloudbuild.yaml create mode 100644 examples/guardrails/folder-factory/.terraform-enterprise/worflows/cloudbuild.yaml diff --git a/examples/guardrails/folder-factory/.cloudbuild/workflows/cloudbuild.yaml b/examples/guardrails/folder-factory/.cloudbuild/workflows/cloudbuild.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/guardrails/folder-factory/.gitlab/workflows/cloudbuild.yaml b/examples/guardrails/folder-factory/.gitlab/workflows/cloudbuild.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/guardrails/folder-factory/.jenkins/workflows/cloudbuild.yaml b/examples/guardrails/folder-factory/.jenkins/workflows/cloudbuild.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/guardrails/folder-factory/.terraform-cloud/workflows/cloudbuild.yaml b/examples/guardrails/folder-factory/.terraform-cloud/workflows/cloudbuild.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/guardrails/folder-factory/.terraform-enterprise/worflows/cloudbuild.yaml b/examples/guardrails/folder-factory/.terraform-enterprise/worflows/cloudbuild.yaml new file mode 100644 index 0000000..e69de29 From 14eeb72a4d044822aa3fbb69b3ac90ddd68a8e88 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Wed, 1 Feb 2023 00:11:25 +0100 Subject: [PATCH 003/132] Combined all GDC pull requests into different folders. --- .../.bitbucket/bitbucket-pipelines.yml | 59 ++ .../{ => bitbucket}/folder-factory/.gitignore | 0 .../{ => bitbucket}/folder-factory/README.md | 0 .../data/folders/folder.yaml.sample | 0 .../bitbucket/folder-factory/get_oidctoken.sh | 7 + .../{ => bitbucket}/folder-factory/main.tf | 0 .../folder-factory/modules/folder/README.md | 0 .../modules/folder/firewall-policies.tf | 0 .../folder-factory/modules/folder/iam.tf | 0 .../folder-factory/modules/folder/logging.tf | 0 .../folder-factory/modules/folder/main.tf | 0 .../modules/folder/organization-policies.tf | 0 .../folder-factory/modules/folder/outputs.tf | 0 .../folder-factory/modules/folder/tags.tf | 0 .../modules/folder/variables.tf | 0 .../folder-factory/modules/folder/versions.tf | 0 .../{ => bitbucket}/folder-factory/outputs.tf | 0 .../folder-factory/provider.tf | 0 .../folder-factory/variables.tf | 0 .../.bitbucket/bitbucket-pipelines.yml | 59 ++ .../project-factory/.gitignore | 0 .../{ => bitbucket}/project-factory/README.md | 0 .../data/projects/project.yaml.sample | 0 .../project-factory/get_oidctoken.sh | 7 + .../bitbucket/project-factory/main.tf | 35 ++ .../project-factory/modules/project/README.md | 0 .../project-factory/modules/project/iam.tf | 0 .../modules/project/logging.tf | 0 .../project-factory/modules/project/main.tf | 0 .../modules/project/organization-policies.tf | 0 .../modules/project/outputs.tf | 0 .../modules/project/service-accounts.tf | 0 .../modules/project/shared-vpc.tf | 0 .../project-factory/modules/project/tags.tf | 0 .../modules/project/variables.tf | 0 .../modules/project/versions.tf | 0 .../project-factory/modules/project/vpc-sc.tf | 0 .../modules/project_plus/README.md | 0 .../modules/project_plus/main.tf | 46 ++ .../modules/project_plus/outputs.tf | 0 .../modules/project_plus/variables.tf | 0 .../modules/project_plus/versions.tf | 0 .../bitbucket/project-factory/outputs.tf | 31 ++ .../project-factory/provider.tf | 0 .../bitbucket/project-factory/variables.tf | 27 + .../bitbucket/project-factory/wif.tf | 50 ++ .../.bitbucket/bitbucket-pipelines.yml | 99 ++++ .../bitbucket/skunkworks/.gitignore | 3 + .../{ => bitbucket}/skunkworks/README.md | 0 .../bitbucket/skunkworks/get_oidctoken.sh | 7 + .../guardrails/bitbucket/skunkworks/main.tf | 23 + .../{ => bitbucket}/skunkworks/provider.tf | 0 .../bitbucket/skunkworks/variables.tf | 19 + .../.cloudbuild/workflows/README.md | 43 ++ .../.cloudbuild/workflows/cloudbuild.yaml | 93 ++++ .../workflows/terraform-deployment.yml | 0 .../cloudbuild/folder-factory/.gitignore | 3 + .../.gitlab}/workflows/cloudbuild.yaml | 0 .../.jenkins}/workflows/cloudbuild.yaml | 0 .../workflows/cloudbuild.yaml | 0 .../worflows}/cloudbuild.yaml | 0 .../cloudbuild/folder-factory/README.md | 59 ++ .../data/folders/folder.yaml.sample | 31 ++ .../cloudbuild/folder-factory/main.tf | 32 ++ .../folder-factory/modules/folder/README.md | 299 ++++++++++ .../modules/folder/firewall-policies.tf | 93 ++++ .../folder-factory/modules/folder/iam.tf | 40 ++ .../folder-factory/modules/folder/logging.tf | 91 +++ .../folder-factory/modules/folder/main.tf | 43 ++ .../modules/folder/organization-policies.tf | 90 +++ .../folder-factory/modules/folder/outputs.tf | 51 ++ .../folder-factory/modules/folder/tags.tf | 21 + .../modules/folder/variables.tf | 151 +++++ .../folder-factory/modules/folder/versions.tf | 29 + .../cloudbuild/folder-factory/outputs.tf | 20 + .../cloudbuild/folder-factory/provider.tf | 28 + .../cloudbuild/folder-factory/variables.tf | 15 + .../.cloudbuild/workflows/README.md | 43 ++ .../.cloudbuild/workflows/cloudbuild.yaml | 93 ++++ .../workflows/terraform-deployment.yml | 0 .../cloudbuild/project-factory/.gitignore | 3 + .../cloudbuild/project-factory/README.md | 75 +++ .../data/projects/project.yaml.sample | 41 ++ .../{ => cloudbuild}/project-factory/main.tf | 0 .../project-factory/modules/project/README.md | 308 +++++++++++ .../project-factory/modules/project/iam.tf | 115 ++++ .../modules/project/logging.tf | 91 +++ .../project-factory/modules/project/main.tf | 95 ++++ .../modules/project/organization-policies.tf | 90 +++ .../modules/project/outputs.tf | 87 +++ .../modules/project/service-accounts.tf | 113 ++++ .../modules/project/shared-vpc.tf | 75 +++ .../project-factory/modules/project/tags.tf | 21 + .../modules/project/variables.tf | 259 +++++++++ .../modules/project/versions.tf | 29 + .../project-factory/modules/project/vpc-sc.tf | 45 ++ .../modules/project_plus/README.md | 1 + .../modules/project_plus/main.tf | 0 .../modules/project_plus/outputs.tf | 36 ++ .../modules/project_plus/variables.tf | 52 ++ .../modules/project_plus/versions.tf | 29 + .../project-factory/outputs.tf | 0 .../cloudbuild/project-factory/provider.tf | 28 + .../project-factory/variables.tf | 0 .../{ => cloudbuild}/project-factory/wif.tf | 0 .../.cloudbuild/workflows/README.md | 47 ++ .../workflows/tf-cloudbuild-dev.yaml | 93 ++++ .../workflows/tf-cloudbuild-prod.yaml | 93 ++++ .../workflows/tf-cloudbuild-stage.yaml | 93 ++++ .../.github/workflows/tf-actions-dev.yml | 0 .../.github/workflows/tf-actions-prod.yml | 0 .../.github/workflows/tf-actions-stage.yml | 0 .../cloudbuild/skunkworks/README.md | 29 + .../{ => cloudbuild}/skunkworks/main.tf | 0 .../cloudbuild/skunkworks/provider.tf | 28 + .../{ => cloudbuild}/skunkworks/variables.tf | 0 .../worflows/cloudbuild.yaml | 0 .../workflows/terraform-deployment.yml | 91 +++ .../github/folder-factory/.gitignore | 3 + .../github/folder-factory/README.md | 59 ++ .../data/folders/folder.yaml.sample | 31 ++ .../guardrails/github/folder-factory/main.tf | 32 ++ .../folder-factory/modules/folder/README.md | 299 ++++++++++ .../modules/folder/firewall-policies.tf | 93 ++++ .../folder-factory/modules/folder/iam.tf | 40 ++ .../folder-factory/modules/folder/logging.tf | 91 +++ .../folder-factory/modules/folder/main.tf | 43 ++ .../modules/folder/organization-policies.tf | 90 +++ .../folder-factory/modules/folder/outputs.tf | 51 ++ .../folder-factory/modules/folder/tags.tf | 21 + .../modules/folder/variables.tf | 151 +++++ .../folder-factory/modules/folder/versions.tf | 29 + .../github/folder-factory/outputs.tf | 20 + .../github/folder-factory/provider.tf | 28 + .../github/folder-factory/variables.tf | 15 + .../workflows/terraform-deployment.yml | 94 ++++ .../github/project-factory/.gitignore | 3 + .../github/project-factory/README.md | 75 +++ .../data/projects/project.yaml.sample | 41 ++ .../guardrails/github/project-factory/main.tf | 35 ++ .../project-factory/modules/project/README.md | 308 +++++++++++ .../project-factory/modules/project/iam.tf | 115 ++++ .../modules/project/logging.tf | 91 +++ .../project-factory/modules/project/main.tf | 95 ++++ .../modules/project/organization-policies.tf | 90 +++ .../modules/project/outputs.tf | 87 +++ .../modules/project/service-accounts.tf | 113 ++++ .../modules/project/shared-vpc.tf | 75 +++ .../project-factory/modules/project/tags.tf | 21 + .../modules/project/variables.tf | 259 +++++++++ .../modules/project/versions.tf | 29 + .../project-factory/modules/project/vpc-sc.tf | 45 ++ .../modules/project_plus/README.md | 1 + .../modules/project_plus/main.tf | 46 ++ .../modules/project_plus/outputs.tf | 36 ++ .../modules/project_plus/variables.tf | 52 ++ .../modules/project_plus/versions.tf | 29 + .../github/project-factory/outputs.tf | 40 ++ .../github/project-factory/provider.tf | 28 + .../github/project-factory/variables.tf | 19 + .../guardrails/github/project-factory/wif.tf | 67 +++ .../.github/workflows/tf-actions-dev.yml | 91 +++ .../.github/workflows/tf-actions-prod.yml | 91 +++ .../.github/workflows/tf-actions-stage.yml | 91 +++ .../guardrails/github/skunkworks/README.md | 29 + examples/guardrails/github/skunkworks/main.tf | 23 + .../guardrails/github/skunkworks/provider.tf | 28 + .../guardrails/github/skunkworks/variables.tf | 20 + .../gitlab/folder-factory/.gitignore | 3 + .../.gitlab/workflows/.gitlab-ci.yml | 134 +++++ .../.gitlab/workflows/README.md | 45 ++ .../gitlab/folder-factory/README.md | 59 ++ .../data/folders/folder.yaml.sample | 31 ++ .../guardrails/gitlab/folder-factory/main.tf | 32 ++ .../folder-factory/modules/folder/README.md | 299 ++++++++++ .../modules/folder/firewall-policies.tf | 93 ++++ .../folder-factory/modules/folder/iam.tf | 40 ++ .../folder-factory/modules/folder/logging.tf | 91 +++ .../folder-factory/modules/folder/main.tf | 43 ++ .../modules/folder/organization-policies.tf | 90 +++ .../folder-factory/modules/folder/outputs.tf | 51 ++ .../folder-factory/modules/folder/tags.tf | 21 + .../modules/folder/variables.tf | 151 +++++ .../folder-factory/modules/folder/versions.tf | 29 + .../gitlab/folder-factory/outputs.tf | 20 + .../gitlab/folder-factory/provider.tf | 28 + .../gitlab/folder-factory/variables.tf | 15 + .../gitlab/project-factory/.gitignore | 3 + .../.gitlab/workflows/.gitlab-ci.yml | 134 +++++ .../.gitlab/workflows/README.md | 45 ++ .../gitlab/project-factory/README.md | 75 +++ .../data/projects/project.yaml.sample | 41 ++ .../guardrails/gitlab/project-factory/main.tf | 35 ++ .../project-factory/modules/project/README.md | 308 +++++++++++ .../project-factory/modules/project/iam.tf | 115 ++++ .../modules/project/logging.tf | 91 +++ .../project-factory/modules/project/main.tf | 95 ++++ .../modules/project/organization-policies.tf | 90 +++ .../modules/project/outputs.tf | 87 +++ .../modules/project/service-accounts.tf | 113 ++++ .../modules/project/shared-vpc.tf | 75 +++ .../project-factory/modules/project/tags.tf | 21 + .../modules/project/variables.tf | 259 +++++++++ .../modules/project/versions.tf | 29 + .../project-factory/modules/project/vpc-sc.tf | 45 ++ .../modules/project_plus/README.md | 1 + .../modules/project_plus/main.tf | 46 ++ .../modules/project_plus/outputs.tf | 36 ++ .../modules/project_plus/variables.tf | 52 ++ .../modules/project_plus/versions.tf | 29 + .../gitlab/project-factory/outputs.tf | 40 ++ .../gitlab/project-factory/provider.tf | 28 + .../gitlab/project-factory/variables.tf | 19 + .../guardrails/gitlab/project-factory/wif.tf | 68 +++ .../.gitlab/workflows/.gitlab-ci.yml | 46 ++ .../skunkworks/.gitlab/workflows/README.md | 54 ++ .../skunkworks/.gitlab/workflows/workflow.yml | 116 ++++ .../guardrails/gitlab/skunkworks/README.md | 29 + examples/guardrails/gitlab/skunkworks/main.tf | 23 + .../guardrails/gitlab/skunkworks/provider.tf | 28 + .../guardrails/gitlab/skunkworks/variables.tf | 20 + .../workflows/terraform-deployment.yml | 91 +++ .../jenkins/folder-factory/.gitignore | 3 + .../jenkins/folder-factory/Jenkinsfile | 70 +++ .../jenkins/folder-factory/README.md | 59 ++ .../data/folders/folder.yaml.sample | 31 ++ .../guardrails/jenkins/folder-factory/main.tf | 32 ++ .../folder-factory/modules/folder/README.md | 299 ++++++++++ .../modules/folder/firewall-policies.tf | 93 ++++ .../folder-factory/modules/folder/iam.tf | 40 ++ .../folder-factory/modules/folder/logging.tf | 91 +++ .../folder-factory/modules/folder/main.tf | 43 ++ .../modules/folder/organization-policies.tf | 90 +++ .../folder-factory/modules/folder/outputs.tf | 51 ++ .../folder-factory/modules/folder/tags.tf | 21 + .../modules/folder/variables.tf | 151 +++++ .../folder-factory/modules/folder/versions.tf | 29 + .../jenkins/folder-factory/outputs.tf | 20 + .../jenkins/folder-factory/provider.tf | 28 + .../jenkins/folder-factory/variables.tf | 15 + .../workflows/terraform-deployment.yml | 94 ++++ .../jenkins/project-factory/.gitignore | 3 + .../jenkins/project-factory/Jenkinsfile | 39 ++ .../jenkins/project-factory/README.md | 75 +++ .../data/projects/project.yaml.sample | 41 ++ .../jenkins/project-factory/main.tf | 36 ++ .../project-factory/modules/project/README.md | 308 +++++++++++ .../project-factory/modules/project/iam.tf | 115 ++++ .../modules/project/logging.tf | 91 +++ .../project-factory/modules/project/main.tf | 95 ++++ .../modules/project/organization-policies.tf | 90 +++ .../modules/project/outputs.tf | 87 +++ .../modules/project/service-accounts.tf | 113 ++++ .../modules/project/shared-vpc.tf | 75 +++ .../project-factory/modules/project/tags.tf | 21 + .../modules/project/variables.tf | 259 +++++++++ .../modules/project/versions.tf | 29 + .../project-factory/modules/project/vpc-sc.tf | 45 ++ .../modules/project_plus/README.md | 1 + .../modules/project_plus/main.tf | 46 ++ .../modules/project_plus/outputs.tf | 36 ++ .../modules/project_plus/variables.tf | 52 ++ .../modules/project_plus/versions.tf | 29 + .../jenkins/project-factory/outputs.tf | 40 ++ .../jenkins/project-factory/provider.tf | 28 + .../jenkins/project-factory/variables.tf | 27 + .../guardrails/jenkins/project-factory/wif.tf | 51 ++ .../.github/workflows/tf-actions-dev.yml | 91 +++ .../.github/workflows/tf-actions-prod.yml | 91 +++ .../.github/workflows/tf-actions-stage.yml | 91 +++ .../guardrails/jenkins/skunkworks/Jenkinsfile | 65 +++ .../guardrails/jenkins/skunkworks/README.md | 29 + .../guardrails/jenkins/skunkworks/main.tf | 23 + .../guardrails/jenkins/skunkworks/provider.tf | 28 + .../jenkins/skunkworks/variables.tf | 20 + .../workflows/terraform-deployment.yml | 91 +++ .../terraform-cloud/folder-factory/.gitignore | 3 + .../terraform-cloud/folder-factory/README.md | 66 +++ .../folder-factory/data/folders/app1.yaml | 31 ++ .../folder-factory/data/folders/app2.yaml | 31 ++ .../terraform-cloud/folder-factory/main.tf | 32 ++ .../folder-factory/modules/folder/README.md | 299 ++++++++++ .../modules/folder/firewall-policies.tf | 93 ++++ .../folder-factory/modules/folder/iam.tf | 40 ++ .../folder-factory/modules/folder/logging.tf | 91 +++ .../folder-factory/modules/folder/main.tf | 43 ++ .../modules/folder/organization-policies.tf | 90 +++ .../folder-factory/modules/folder/outputs.tf | 51 ++ .../folder-factory/modules/folder/tags.tf | 21 + .../modules/folder/variables.tf | 151 +++++ .../folder-factory/modules/folder/versions.tf | 29 + .../terraform-cloud/folder-factory/outputs.tf | 20 + .../folder-factory/provider.tf | 31 ++ .../terraform-cloud-wif/README.md | 115 ++++ .../terraform-cloud-wif/diagram.png | Bin 0 -> 29084 bytes .../gcp-workload-identity-provider/README.md | 33 ++ .../iam-service-account/README.md | 75 +++ .../iam-service-account/iam.tf | 145 +++++ .../iam-service-account/main.tf | 87 +++ .../iam-service-account/outputs.tf | 65 +++ .../iam-service-account/variables.tf | 121 ++++ .../iam-service-account/versions.tf | 29 + .../gcp-workload-identity-provider/main.tf | 99 ++++ .../gcp-workload-identity-provider/outputs.tf | 35 ++ .../project/README.md | 522 ++++++++++++++++++ .../project/iam.tf | 115 ++++ .../project/logging.tf | 103 ++++ .../project/main.tf | 95 ++++ .../project/organization-policies.tf | 142 +++++ .../project/outputs.tf | 94 ++++ .../project/service-accounts.tf | 152 +++++ .../project/shared-vpc.tf | 76 +++ .../project/tags.tf | 21 + .../project/variables.tf | 305 ++++++++++ .../project/versions.tf | 29 + .../project/vpc-sc.tf | 45 ++ .../terraform.auto.tfvars | 24 + .../terraform.auto.tfvars.template | 20 + .../terraform.tfstate | 219 ++++++++ .../terraform.tfstate.backup | 219 ++++++++ .../variables.tf | 68 +++ .../folder-factory/tfc-oidc/README.md | 38 ++ .../folder-factory/tfc-oidc/get_audience.sh | 23 + .../folder-factory/tfc-oidc/main.tf | 23 + .../folder-factory/tfc-oidc/outputs.tf | 27 + .../folder-factory/tfc-oidc/variables.tf | 26 + .../folder-factory/tfc-oidc/versions.tf | 17 + .../folder-factory/tfc-oidc/write_token.sh | 23 + .../folder-factory/variables.tf | 25 + .../workflows/terraform-deployment.yml | 94 ++++ .../project-factory/.gitignore | 3 + .../terraform-cloud/project-factory/README.md | 80 +++ .../data/projects/dev-skunkworks.yaml | 42 ++ .../data/projects/prod1-skunkworks.yaml | 42 ++ .../projects/stage-skunkworks.yaml.template | 43 ++ .../terraform-cloud/project-factory/main.tf | 39 ++ .../project-factory/modules/project/README.md | 308 +++++++++++ .../project-factory/modules/project/iam.tf | 115 ++++ .../modules/project/logging.tf | 91 +++ .../project-factory/modules/project/main.tf | 95 ++++ .../modules/project/organization-policies.tf | 90 +++ .../modules/project/outputs.tf | 87 +++ .../modules/project/service-accounts.tf | 113 ++++ .../modules/project/shared-vpc.tf | 75 +++ .../project-factory/modules/project/tags.tf | 21 + .../modules/project/variables.tf | 259 +++++++++ .../modules/project/versions.tf | 29 + .../project-factory/modules/project/vpc-sc.tf | 45 ++ .../modules/project_plus/README.md | 1 + .../modules/project_plus/main.tf | 47 ++ .../modules/project_plus/outputs.tf | 36 ++ .../modules/project_plus/variables.tf | 57 ++ .../modules/project_plus/versions.tf | 29 + .../project-factory/outputs.tf | 45 ++ .../project-factory/provider.tf | 44 ++ .../project-factory/tfc-oidc/README.md | 38 ++ .../project-factory/tfc-oidc/get_audience.sh | 23 + .../project-factory/tfc-oidc/main.tf | 23 + .../project-factory/tfc-oidc/outputs.tf | 27 + .../project-factory/tfc-oidc/variables.tf | 26 + .../project-factory/tfc-oidc/versions.tf | 17 + .../project-factory/tfc-oidc/write_token.sh | 23 + .../project-factory/variables.tf | 26 + .../terraform-cloud/project-factory/wif.tf | 83 +++ .../.github/workflows/tf-actions-dev.yml | 91 +++ .../.github/workflows/tf-actions-prod.yml | 91 +++ .../.github/workflows/tf-actions-stage.yml | 91 +++ .../terraform-cloud/skunkworks/README.md | 37 ++ .../terraform-cloud/skunkworks/main.tf | 22 + .../terraform-cloud/skunkworks/provider.tf | 30 + .../skunkworks/tfc-oidc/README.md | 38 ++ .../skunkworks/tfc-oidc/get_audience.sh | 23 + .../skunkworks/tfc-oidc/main.tf | 23 + .../skunkworks/tfc-oidc/outputs.tf | 27 + .../skunkworks/tfc-oidc/variables.tf | 26 + .../skunkworks/tfc-oidc/versions.tf | 17 + .../skunkworks/tfc-oidc/write_token.sh | 23 + .../terraform-cloud/skunkworks/variables.tf | 26 + 378 files changed, 21711 insertions(+) create mode 100644 examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml rename examples/guardrails/{ => bitbucket}/folder-factory/.gitignore (100%) rename examples/guardrails/{ => bitbucket}/folder-factory/README.md (100%) rename examples/guardrails/{ => bitbucket}/folder-factory/data/folders/folder.yaml.sample (100%) create mode 100755 examples/guardrails/bitbucket/folder-factory/get_oidctoken.sh rename examples/guardrails/{ => bitbucket}/folder-factory/main.tf (100%) rename examples/guardrails/{ => bitbucket}/folder-factory/modules/folder/README.md (100%) rename examples/guardrails/{ => bitbucket}/folder-factory/modules/folder/firewall-policies.tf (100%) rename examples/guardrails/{ => bitbucket}/folder-factory/modules/folder/iam.tf (100%) rename examples/guardrails/{ => bitbucket}/folder-factory/modules/folder/logging.tf (100%) rename examples/guardrails/{ => bitbucket}/folder-factory/modules/folder/main.tf (100%) rename examples/guardrails/{ => bitbucket}/folder-factory/modules/folder/organization-policies.tf (100%) rename examples/guardrails/{ => bitbucket}/folder-factory/modules/folder/outputs.tf (100%) rename examples/guardrails/{ => bitbucket}/folder-factory/modules/folder/tags.tf (100%) rename examples/guardrails/{ => bitbucket}/folder-factory/modules/folder/variables.tf (100%) rename examples/guardrails/{ => bitbucket}/folder-factory/modules/folder/versions.tf (100%) rename examples/guardrails/{ => bitbucket}/folder-factory/outputs.tf (100%) rename examples/guardrails/{ => bitbucket}/folder-factory/provider.tf (100%) rename examples/guardrails/{ => bitbucket}/folder-factory/variables.tf (100%) create mode 100644 examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml rename examples/guardrails/{ => bitbucket}/project-factory/.gitignore (100%) rename examples/guardrails/{ => bitbucket}/project-factory/README.md (100%) rename examples/guardrails/{ => bitbucket}/project-factory/data/projects/project.yaml.sample (100%) create mode 100755 examples/guardrails/bitbucket/project-factory/get_oidctoken.sh create mode 100644 examples/guardrails/bitbucket/project-factory/main.tf rename examples/guardrails/{ => bitbucket}/project-factory/modules/project/README.md (100%) rename examples/guardrails/{ => bitbucket}/project-factory/modules/project/iam.tf (100%) rename examples/guardrails/{ => bitbucket}/project-factory/modules/project/logging.tf (100%) rename examples/guardrails/{ => bitbucket}/project-factory/modules/project/main.tf (100%) rename examples/guardrails/{ => bitbucket}/project-factory/modules/project/organization-policies.tf (100%) rename examples/guardrails/{ => bitbucket}/project-factory/modules/project/outputs.tf (100%) rename examples/guardrails/{ => bitbucket}/project-factory/modules/project/service-accounts.tf (100%) rename examples/guardrails/{ => bitbucket}/project-factory/modules/project/shared-vpc.tf (100%) rename examples/guardrails/{ => bitbucket}/project-factory/modules/project/tags.tf (100%) rename examples/guardrails/{ => bitbucket}/project-factory/modules/project/variables.tf (100%) rename examples/guardrails/{ => bitbucket}/project-factory/modules/project/versions.tf (100%) rename examples/guardrails/{ => bitbucket}/project-factory/modules/project/vpc-sc.tf (100%) rename examples/guardrails/{ => bitbucket}/project-factory/modules/project_plus/README.md (100%) create mode 100644 examples/guardrails/bitbucket/project-factory/modules/project_plus/main.tf rename examples/guardrails/{ => bitbucket}/project-factory/modules/project_plus/outputs.tf (100%) rename examples/guardrails/{ => bitbucket}/project-factory/modules/project_plus/variables.tf (100%) rename examples/guardrails/{ => bitbucket}/project-factory/modules/project_plus/versions.tf (100%) create mode 100644 examples/guardrails/bitbucket/project-factory/outputs.tf rename examples/guardrails/{ => bitbucket}/project-factory/provider.tf (100%) create mode 100644 examples/guardrails/bitbucket/project-factory/variables.tf create mode 100644 examples/guardrails/bitbucket/project-factory/wif.tf create mode 100644 examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml create mode 100644 examples/guardrails/bitbucket/skunkworks/.gitignore rename examples/guardrails/{ => bitbucket}/skunkworks/README.md (100%) create mode 100755 examples/guardrails/bitbucket/skunkworks/get_oidctoken.sh create mode 100644 examples/guardrails/bitbucket/skunkworks/main.tf rename examples/guardrails/{ => bitbucket}/skunkworks/provider.tf (100%) create mode 100644 examples/guardrails/bitbucket/skunkworks/variables.tf create mode 100644 examples/guardrails/cloudbuild/folder-factory/.cloudbuild/workflows/README.md create mode 100644 examples/guardrails/cloudbuild/folder-factory/.cloudbuild/workflows/cloudbuild.yaml rename examples/guardrails/{ => cloudbuild}/folder-factory/.github/workflows/terraform-deployment.yml (100%) create mode 100644 examples/guardrails/cloudbuild/folder-factory/.gitignore rename examples/guardrails/{folder-factory/.cloudbuild => cloudbuild/folder-factory/.gitlab}/workflows/cloudbuild.yaml (100%) rename examples/guardrails/{folder-factory/.gitlab => cloudbuild/folder-factory/.jenkins}/workflows/cloudbuild.yaml (100%) rename examples/guardrails/{folder-factory/.jenkins => cloudbuild/folder-factory/.terraform-cloud}/workflows/cloudbuild.yaml (100%) rename examples/guardrails/{folder-factory/.terraform-cloud/workflows => cloudbuild/folder-factory/.terraform-enterprise/worflows}/cloudbuild.yaml (100%) create mode 100644 examples/guardrails/cloudbuild/folder-factory/README.md create mode 100644 examples/guardrails/cloudbuild/folder-factory/data/folders/folder.yaml.sample create mode 100644 examples/guardrails/cloudbuild/folder-factory/main.tf create mode 100644 examples/guardrails/cloudbuild/folder-factory/modules/folder/README.md create mode 100644 examples/guardrails/cloudbuild/folder-factory/modules/folder/firewall-policies.tf create mode 100644 examples/guardrails/cloudbuild/folder-factory/modules/folder/iam.tf create mode 100644 examples/guardrails/cloudbuild/folder-factory/modules/folder/logging.tf create mode 100644 examples/guardrails/cloudbuild/folder-factory/modules/folder/main.tf create mode 100644 examples/guardrails/cloudbuild/folder-factory/modules/folder/organization-policies.tf create mode 100644 examples/guardrails/cloudbuild/folder-factory/modules/folder/outputs.tf create mode 100644 examples/guardrails/cloudbuild/folder-factory/modules/folder/tags.tf create mode 100644 examples/guardrails/cloudbuild/folder-factory/modules/folder/variables.tf create mode 100644 examples/guardrails/cloudbuild/folder-factory/modules/folder/versions.tf create mode 100644 examples/guardrails/cloudbuild/folder-factory/outputs.tf create mode 100644 examples/guardrails/cloudbuild/folder-factory/provider.tf create mode 100644 examples/guardrails/cloudbuild/folder-factory/variables.tf create mode 100644 examples/guardrails/cloudbuild/project-factory/.cloudbuild/workflows/README.md create mode 100644 examples/guardrails/cloudbuild/project-factory/.cloudbuild/workflows/cloudbuild.yaml rename examples/guardrails/{ => cloudbuild}/project-factory/.github/workflows/terraform-deployment.yml (100%) create mode 100644 examples/guardrails/cloudbuild/project-factory/.gitignore create mode 100644 examples/guardrails/cloudbuild/project-factory/README.md create mode 100644 examples/guardrails/cloudbuild/project-factory/data/projects/project.yaml.sample rename examples/guardrails/{ => cloudbuild}/project-factory/main.tf (100%) create mode 100644 examples/guardrails/cloudbuild/project-factory/modules/project/README.md create mode 100644 examples/guardrails/cloudbuild/project-factory/modules/project/iam.tf create mode 100644 examples/guardrails/cloudbuild/project-factory/modules/project/logging.tf create mode 100644 examples/guardrails/cloudbuild/project-factory/modules/project/main.tf create mode 100644 examples/guardrails/cloudbuild/project-factory/modules/project/organization-policies.tf create mode 100644 examples/guardrails/cloudbuild/project-factory/modules/project/outputs.tf create mode 100644 examples/guardrails/cloudbuild/project-factory/modules/project/service-accounts.tf create mode 100644 examples/guardrails/cloudbuild/project-factory/modules/project/shared-vpc.tf create mode 100644 examples/guardrails/cloudbuild/project-factory/modules/project/tags.tf create mode 100644 examples/guardrails/cloudbuild/project-factory/modules/project/variables.tf create mode 100644 examples/guardrails/cloudbuild/project-factory/modules/project/versions.tf create mode 100644 examples/guardrails/cloudbuild/project-factory/modules/project/vpc-sc.tf create mode 100644 examples/guardrails/cloudbuild/project-factory/modules/project_plus/README.md rename examples/guardrails/{ => cloudbuild}/project-factory/modules/project_plus/main.tf (100%) create mode 100644 examples/guardrails/cloudbuild/project-factory/modules/project_plus/outputs.tf create mode 100644 examples/guardrails/cloudbuild/project-factory/modules/project_plus/variables.tf create mode 100644 examples/guardrails/cloudbuild/project-factory/modules/project_plus/versions.tf rename examples/guardrails/{ => cloudbuild}/project-factory/outputs.tf (100%) create mode 100644 examples/guardrails/cloudbuild/project-factory/provider.tf rename examples/guardrails/{ => cloudbuild}/project-factory/variables.tf (100%) rename examples/guardrails/{ => cloudbuild}/project-factory/wif.tf (100%) create mode 100644 examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/README.md create mode 100644 examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/tf-cloudbuild-dev.yaml create mode 100644 examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/tf-cloudbuild-prod.yaml create mode 100644 examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/tf-cloudbuild-stage.yaml rename examples/guardrails/{ => cloudbuild}/skunkworks/.github/workflows/tf-actions-dev.yml (100%) rename examples/guardrails/{ => cloudbuild}/skunkworks/.github/workflows/tf-actions-prod.yml (100%) rename examples/guardrails/{ => cloudbuild}/skunkworks/.github/workflows/tf-actions-stage.yml (100%) create mode 100644 examples/guardrails/cloudbuild/skunkworks/README.md rename examples/guardrails/{ => cloudbuild}/skunkworks/main.tf (100%) create mode 100644 examples/guardrails/cloudbuild/skunkworks/provider.tf rename examples/guardrails/{ => cloudbuild}/skunkworks/variables.tf (100%) delete mode 100644 examples/guardrails/folder-factory/.terraform-enterprise/worflows/cloudbuild.yaml create mode 100644 examples/guardrails/github/folder-factory/.github/workflows/terraform-deployment.yml create mode 100644 examples/guardrails/github/folder-factory/.gitignore create mode 100644 examples/guardrails/github/folder-factory/README.md create mode 100644 examples/guardrails/github/folder-factory/data/folders/folder.yaml.sample create mode 100644 examples/guardrails/github/folder-factory/main.tf create mode 100644 examples/guardrails/github/folder-factory/modules/folder/README.md create mode 100644 examples/guardrails/github/folder-factory/modules/folder/firewall-policies.tf create mode 100644 examples/guardrails/github/folder-factory/modules/folder/iam.tf create mode 100644 examples/guardrails/github/folder-factory/modules/folder/logging.tf create mode 100644 examples/guardrails/github/folder-factory/modules/folder/main.tf create mode 100644 examples/guardrails/github/folder-factory/modules/folder/organization-policies.tf create mode 100644 examples/guardrails/github/folder-factory/modules/folder/outputs.tf create mode 100644 examples/guardrails/github/folder-factory/modules/folder/tags.tf create mode 100644 examples/guardrails/github/folder-factory/modules/folder/variables.tf create mode 100644 examples/guardrails/github/folder-factory/modules/folder/versions.tf create mode 100644 examples/guardrails/github/folder-factory/outputs.tf create mode 100644 examples/guardrails/github/folder-factory/provider.tf create mode 100644 examples/guardrails/github/folder-factory/variables.tf create mode 100644 examples/guardrails/github/project-factory/.github/workflows/terraform-deployment.yml create mode 100644 examples/guardrails/github/project-factory/.gitignore create mode 100644 examples/guardrails/github/project-factory/README.md create mode 100644 examples/guardrails/github/project-factory/data/projects/project.yaml.sample create mode 100644 examples/guardrails/github/project-factory/main.tf create mode 100644 examples/guardrails/github/project-factory/modules/project/README.md create mode 100644 examples/guardrails/github/project-factory/modules/project/iam.tf create mode 100644 examples/guardrails/github/project-factory/modules/project/logging.tf create mode 100644 examples/guardrails/github/project-factory/modules/project/main.tf create mode 100644 examples/guardrails/github/project-factory/modules/project/organization-policies.tf create mode 100644 examples/guardrails/github/project-factory/modules/project/outputs.tf create mode 100644 examples/guardrails/github/project-factory/modules/project/service-accounts.tf create mode 100644 examples/guardrails/github/project-factory/modules/project/shared-vpc.tf create mode 100644 examples/guardrails/github/project-factory/modules/project/tags.tf create mode 100644 examples/guardrails/github/project-factory/modules/project/variables.tf create mode 100644 examples/guardrails/github/project-factory/modules/project/versions.tf create mode 100644 examples/guardrails/github/project-factory/modules/project/vpc-sc.tf create mode 100644 examples/guardrails/github/project-factory/modules/project_plus/README.md create mode 100644 examples/guardrails/github/project-factory/modules/project_plus/main.tf create mode 100644 examples/guardrails/github/project-factory/modules/project_plus/outputs.tf create mode 100644 examples/guardrails/github/project-factory/modules/project_plus/variables.tf create mode 100644 examples/guardrails/github/project-factory/modules/project_plus/versions.tf create mode 100644 examples/guardrails/github/project-factory/outputs.tf create mode 100644 examples/guardrails/github/project-factory/provider.tf create mode 100644 examples/guardrails/github/project-factory/variables.tf create mode 100644 examples/guardrails/github/project-factory/wif.tf create mode 100644 examples/guardrails/github/skunkworks/.github/workflows/tf-actions-dev.yml create mode 100644 examples/guardrails/github/skunkworks/.github/workflows/tf-actions-prod.yml create mode 100644 examples/guardrails/github/skunkworks/.github/workflows/tf-actions-stage.yml create mode 100644 examples/guardrails/github/skunkworks/README.md create mode 100644 examples/guardrails/github/skunkworks/main.tf create mode 100644 examples/guardrails/github/skunkworks/provider.tf create mode 100644 examples/guardrails/github/skunkworks/variables.tf create mode 100644 examples/guardrails/gitlab/folder-factory/.gitignore create mode 100644 examples/guardrails/gitlab/folder-factory/.gitlab/workflows/.gitlab-ci.yml create mode 100644 examples/guardrails/gitlab/folder-factory/.gitlab/workflows/README.md create mode 100644 examples/guardrails/gitlab/folder-factory/README.md create mode 100644 examples/guardrails/gitlab/folder-factory/data/folders/folder.yaml.sample create mode 100644 examples/guardrails/gitlab/folder-factory/main.tf create mode 100644 examples/guardrails/gitlab/folder-factory/modules/folder/README.md create mode 100644 examples/guardrails/gitlab/folder-factory/modules/folder/firewall-policies.tf create mode 100644 examples/guardrails/gitlab/folder-factory/modules/folder/iam.tf create mode 100644 examples/guardrails/gitlab/folder-factory/modules/folder/logging.tf create mode 100644 examples/guardrails/gitlab/folder-factory/modules/folder/main.tf create mode 100644 examples/guardrails/gitlab/folder-factory/modules/folder/organization-policies.tf create mode 100644 examples/guardrails/gitlab/folder-factory/modules/folder/outputs.tf create mode 100644 examples/guardrails/gitlab/folder-factory/modules/folder/tags.tf create mode 100644 examples/guardrails/gitlab/folder-factory/modules/folder/variables.tf create mode 100644 examples/guardrails/gitlab/folder-factory/modules/folder/versions.tf create mode 100644 examples/guardrails/gitlab/folder-factory/outputs.tf create mode 100644 examples/guardrails/gitlab/folder-factory/provider.tf create mode 100644 examples/guardrails/gitlab/folder-factory/variables.tf create mode 100644 examples/guardrails/gitlab/project-factory/.gitignore create mode 100644 examples/guardrails/gitlab/project-factory/.gitlab/workflows/.gitlab-ci.yml create mode 100644 examples/guardrails/gitlab/project-factory/.gitlab/workflows/README.md create mode 100644 examples/guardrails/gitlab/project-factory/README.md create mode 100644 examples/guardrails/gitlab/project-factory/data/projects/project.yaml.sample create mode 100644 examples/guardrails/gitlab/project-factory/main.tf create mode 100644 examples/guardrails/gitlab/project-factory/modules/project/README.md create mode 100644 examples/guardrails/gitlab/project-factory/modules/project/iam.tf create mode 100644 examples/guardrails/gitlab/project-factory/modules/project/logging.tf create mode 100644 examples/guardrails/gitlab/project-factory/modules/project/main.tf create mode 100644 examples/guardrails/gitlab/project-factory/modules/project/organization-policies.tf create mode 100644 examples/guardrails/gitlab/project-factory/modules/project/outputs.tf create mode 100644 examples/guardrails/gitlab/project-factory/modules/project/service-accounts.tf create mode 100644 examples/guardrails/gitlab/project-factory/modules/project/shared-vpc.tf create mode 100644 examples/guardrails/gitlab/project-factory/modules/project/tags.tf create mode 100644 examples/guardrails/gitlab/project-factory/modules/project/variables.tf create mode 100644 examples/guardrails/gitlab/project-factory/modules/project/versions.tf create mode 100644 examples/guardrails/gitlab/project-factory/modules/project/vpc-sc.tf create mode 100644 examples/guardrails/gitlab/project-factory/modules/project_plus/README.md create mode 100644 examples/guardrails/gitlab/project-factory/modules/project_plus/main.tf create mode 100644 examples/guardrails/gitlab/project-factory/modules/project_plus/outputs.tf create mode 100644 examples/guardrails/gitlab/project-factory/modules/project_plus/variables.tf create mode 100644 examples/guardrails/gitlab/project-factory/modules/project_plus/versions.tf create mode 100644 examples/guardrails/gitlab/project-factory/outputs.tf create mode 100644 examples/guardrails/gitlab/project-factory/provider.tf create mode 100644 examples/guardrails/gitlab/project-factory/variables.tf create mode 100644 examples/guardrails/gitlab/project-factory/wif.tf create mode 100644 examples/guardrails/gitlab/skunkworks/.gitlab/workflows/.gitlab-ci.yml create mode 100644 examples/guardrails/gitlab/skunkworks/.gitlab/workflows/README.md create mode 100644 examples/guardrails/gitlab/skunkworks/.gitlab/workflows/workflow.yml create mode 100644 examples/guardrails/gitlab/skunkworks/README.md create mode 100644 examples/guardrails/gitlab/skunkworks/main.tf create mode 100644 examples/guardrails/gitlab/skunkworks/provider.tf create mode 100644 examples/guardrails/gitlab/skunkworks/variables.tf create mode 100644 examples/guardrails/jenkins/folder-factory/.github/workflows/terraform-deployment.yml create mode 100644 examples/guardrails/jenkins/folder-factory/.gitignore create mode 100644 examples/guardrails/jenkins/folder-factory/Jenkinsfile create mode 100644 examples/guardrails/jenkins/folder-factory/README.md create mode 100644 examples/guardrails/jenkins/folder-factory/data/folders/folder.yaml.sample create mode 100644 examples/guardrails/jenkins/folder-factory/main.tf create mode 100644 examples/guardrails/jenkins/folder-factory/modules/folder/README.md create mode 100644 examples/guardrails/jenkins/folder-factory/modules/folder/firewall-policies.tf create mode 100644 examples/guardrails/jenkins/folder-factory/modules/folder/iam.tf create mode 100644 examples/guardrails/jenkins/folder-factory/modules/folder/logging.tf create mode 100644 examples/guardrails/jenkins/folder-factory/modules/folder/main.tf create mode 100644 examples/guardrails/jenkins/folder-factory/modules/folder/organization-policies.tf create mode 100644 examples/guardrails/jenkins/folder-factory/modules/folder/outputs.tf create mode 100644 examples/guardrails/jenkins/folder-factory/modules/folder/tags.tf create mode 100644 examples/guardrails/jenkins/folder-factory/modules/folder/variables.tf create mode 100644 examples/guardrails/jenkins/folder-factory/modules/folder/versions.tf create mode 100644 examples/guardrails/jenkins/folder-factory/outputs.tf create mode 100644 examples/guardrails/jenkins/folder-factory/provider.tf create mode 100644 examples/guardrails/jenkins/folder-factory/variables.tf create mode 100644 examples/guardrails/jenkins/project-factory/.github/workflows/terraform-deployment.yml create mode 100644 examples/guardrails/jenkins/project-factory/.gitignore create mode 100644 examples/guardrails/jenkins/project-factory/Jenkinsfile create mode 100644 examples/guardrails/jenkins/project-factory/README.md create mode 100644 examples/guardrails/jenkins/project-factory/data/projects/project.yaml.sample create mode 100644 examples/guardrails/jenkins/project-factory/main.tf create mode 100644 examples/guardrails/jenkins/project-factory/modules/project/README.md create mode 100644 examples/guardrails/jenkins/project-factory/modules/project/iam.tf create mode 100644 examples/guardrails/jenkins/project-factory/modules/project/logging.tf create mode 100644 examples/guardrails/jenkins/project-factory/modules/project/main.tf create mode 100644 examples/guardrails/jenkins/project-factory/modules/project/organization-policies.tf create mode 100644 examples/guardrails/jenkins/project-factory/modules/project/outputs.tf create mode 100644 examples/guardrails/jenkins/project-factory/modules/project/service-accounts.tf create mode 100644 examples/guardrails/jenkins/project-factory/modules/project/shared-vpc.tf create mode 100644 examples/guardrails/jenkins/project-factory/modules/project/tags.tf create mode 100644 examples/guardrails/jenkins/project-factory/modules/project/variables.tf create mode 100644 examples/guardrails/jenkins/project-factory/modules/project/versions.tf create mode 100644 examples/guardrails/jenkins/project-factory/modules/project/vpc-sc.tf create mode 100644 examples/guardrails/jenkins/project-factory/modules/project_plus/README.md create mode 100644 examples/guardrails/jenkins/project-factory/modules/project_plus/main.tf create mode 100644 examples/guardrails/jenkins/project-factory/modules/project_plus/outputs.tf create mode 100644 examples/guardrails/jenkins/project-factory/modules/project_plus/variables.tf create mode 100644 examples/guardrails/jenkins/project-factory/modules/project_plus/versions.tf create mode 100644 examples/guardrails/jenkins/project-factory/outputs.tf create mode 100644 examples/guardrails/jenkins/project-factory/provider.tf create mode 100644 examples/guardrails/jenkins/project-factory/variables.tf create mode 100644 examples/guardrails/jenkins/project-factory/wif.tf create mode 100644 examples/guardrails/jenkins/skunkworks/.github/workflows/tf-actions-dev.yml create mode 100644 examples/guardrails/jenkins/skunkworks/.github/workflows/tf-actions-prod.yml create mode 100644 examples/guardrails/jenkins/skunkworks/.github/workflows/tf-actions-stage.yml create mode 100644 examples/guardrails/jenkins/skunkworks/Jenkinsfile create mode 100644 examples/guardrails/jenkins/skunkworks/README.md create mode 100644 examples/guardrails/jenkins/skunkworks/main.tf create mode 100644 examples/guardrails/jenkins/skunkworks/provider.tf create mode 100644 examples/guardrails/jenkins/skunkworks/variables.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/.github/workflows/terraform-deployment.yml create mode 100644 examples/guardrails/terraform-cloud/folder-factory/.gitignore create mode 100644 examples/guardrails/terraform-cloud/folder-factory/README.md create mode 100644 examples/guardrails/terraform-cloud/folder-factory/data/folders/app1.yaml create mode 100644 examples/guardrails/terraform-cloud/folder-factory/data/folders/app2.yaml create mode 100644 examples/guardrails/terraform-cloud/folder-factory/main.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/modules/folder/README.md create mode 100644 examples/guardrails/terraform-cloud/folder-factory/modules/folder/firewall-policies.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/modules/folder/iam.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/modules/folder/logging.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/modules/folder/main.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/modules/folder/organization-policies.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/modules/folder/outputs.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/modules/folder/tags.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/modules/folder/variables.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/modules/folder/versions.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/outputs.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/provider.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/README.md create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/diagram.png create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/README.md create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/README.md create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/iam.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/main.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/outputs.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/variables.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/versions.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/main.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/outputs.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/README.md create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/iam.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/logging.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/main.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/organization-policies.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/outputs.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/service-accounts.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/shared-vpc.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/tags.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/variables.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/versions.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/vpc-sc.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/terraform.auto.tfvars create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/terraform.auto.tfvars.template create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/terraform.tfstate create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/terraform.tfstate.backup create mode 100644 examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/variables.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/tfc-oidc/README.md create mode 100644 examples/guardrails/terraform-cloud/folder-factory/tfc-oidc/get_audience.sh create mode 100644 examples/guardrails/terraform-cloud/folder-factory/tfc-oidc/main.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/tfc-oidc/outputs.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/tfc-oidc/variables.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/tfc-oidc/versions.tf create mode 100644 examples/guardrails/terraform-cloud/folder-factory/tfc-oidc/write_token.sh create mode 100644 examples/guardrails/terraform-cloud/folder-factory/variables.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/.github/workflows/terraform-deployment.yml create mode 100644 examples/guardrails/terraform-cloud/project-factory/.gitignore create mode 100644 examples/guardrails/terraform-cloud/project-factory/README.md create mode 100644 examples/guardrails/terraform-cloud/project-factory/data/projects/dev-skunkworks.yaml create mode 100644 examples/guardrails/terraform-cloud/project-factory/data/projects/prod1-skunkworks.yaml create mode 100644 examples/guardrails/terraform-cloud/project-factory/data/projects/stage-skunkworks.yaml.template create mode 100644 examples/guardrails/terraform-cloud/project-factory/main.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project/README.md create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project/iam.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project/logging.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project/main.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project/organization-policies.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project/outputs.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project/service-accounts.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project/shared-vpc.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project/tags.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project/variables.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project/versions.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project/vpc-sc.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project_plus/README.md create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project_plus/main.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project_plus/outputs.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project_plus/variables.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/modules/project_plus/versions.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/outputs.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/provider.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/tfc-oidc/README.md create mode 100644 examples/guardrails/terraform-cloud/project-factory/tfc-oidc/get_audience.sh create mode 100644 examples/guardrails/terraform-cloud/project-factory/tfc-oidc/main.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/tfc-oidc/outputs.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/tfc-oidc/variables.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/tfc-oidc/versions.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/tfc-oidc/write_token.sh create mode 100644 examples/guardrails/terraform-cloud/project-factory/variables.tf create mode 100644 examples/guardrails/terraform-cloud/project-factory/wif.tf create mode 100644 examples/guardrails/terraform-cloud/skunkworks/.github/workflows/tf-actions-dev.yml create mode 100644 examples/guardrails/terraform-cloud/skunkworks/.github/workflows/tf-actions-prod.yml create mode 100644 examples/guardrails/terraform-cloud/skunkworks/.github/workflows/tf-actions-stage.yml create mode 100644 examples/guardrails/terraform-cloud/skunkworks/README.md create mode 100644 examples/guardrails/terraform-cloud/skunkworks/main.tf create mode 100644 examples/guardrails/terraform-cloud/skunkworks/provider.tf create mode 100644 examples/guardrails/terraform-cloud/skunkworks/tfc-oidc/README.md create mode 100644 examples/guardrails/terraform-cloud/skunkworks/tfc-oidc/get_audience.sh create mode 100644 examples/guardrails/terraform-cloud/skunkworks/tfc-oidc/main.tf create mode 100644 examples/guardrails/terraform-cloud/skunkworks/tfc-oidc/outputs.tf create mode 100644 examples/guardrails/terraform-cloud/skunkworks/tfc-oidc/variables.tf create mode 100644 examples/guardrails/terraform-cloud/skunkworks/tfc-oidc/versions.tf create mode 100644 examples/guardrails/terraform-cloud/skunkworks/tfc-oidc/write_token.sh create mode 100644 examples/guardrails/terraform-cloud/skunkworks/variables.tf diff --git a/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml new file mode 100644 index 0000000..21bbc34 --- /dev/null +++ b/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml @@ -0,0 +1,59 @@ +image: google/cloud-sdk +pipelines: + default: + - step: + name: setup terraform + script: + - apt-get update && apt-get install unzip wget -y + - wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip || echo fine... + - unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip + - yes | cp terraform /usr/bin/ + - ls /usr/bin + artifacts: + - terraform + - step: + oidc: true + name: configure oidc + script: + - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - ./get_oidctoken.sh + artifacts: + - sts-creds.json + - /tmp/gcp_access_token.out + - step: + name: terraform init + oidc: true + script: + - cp terraform /usr/bin/ + - ls . /usr/bin/ + - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - ./get_oidctoken.sh + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json + - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - /usr/bin/terraform plan + branches: + master: + - step: + name: setup terraform + script: + - apt-get update && apt-get install unzip wget -y + - wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip || echo fine... + - unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip + - yes | cp terraform /usr/bin/ + - ls /usr/bin + artifacts: + - terraform + - step: + name: master branch execution tf-apply + oidc: true + script: + - cp terraform /usr/bin/ + - ls . /usr/bin/ + - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - ./get_oidctoken.sh + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json + - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - /usr/bin/terraform plan + - /usr/bin/terraform apply -auto-approve + + diff --git a/examples/guardrails/folder-factory/.gitignore b/examples/guardrails/bitbucket/folder-factory/.gitignore similarity index 100% rename from examples/guardrails/folder-factory/.gitignore rename to examples/guardrails/bitbucket/folder-factory/.gitignore diff --git a/examples/guardrails/folder-factory/README.md b/examples/guardrails/bitbucket/folder-factory/README.md similarity index 100% rename from examples/guardrails/folder-factory/README.md rename to examples/guardrails/bitbucket/folder-factory/README.md diff --git a/examples/guardrails/folder-factory/data/folders/folder.yaml.sample b/examples/guardrails/bitbucket/folder-factory/data/folders/folder.yaml.sample similarity index 100% rename from examples/guardrails/folder-factory/data/folders/folder.yaml.sample rename to examples/guardrails/bitbucket/folder-factory/data/folders/folder.yaml.sample diff --git a/examples/guardrails/bitbucket/folder-factory/get_oidctoken.sh b/examples/guardrails/bitbucket/folder-factory/get_oidctoken.sh new file mode 100755 index 0000000..b032b70 --- /dev/null +++ b/examples/guardrails/bitbucket/folder-factory/get_oidctoken.sh @@ -0,0 +1,7 @@ +#!/bin/bash +echo -n "${BITBUCKET_STEP_OIDC_TOKEN}" > /tmp/gcp_access_token.out +gcloud iam workload-identity-pools create-cred-config "projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/${workload_identity_pool_id}/providers/${workload_identity_pool_provider_id}" --credential-source-file=/tmp/gcp_access_token.out --service-account="${SERVICE_ACCOUNT_EMAIL}" --output-file=sts-creds.json +export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json +gcloud auth login --cred-file=`pwd`/sts-creds.json +gcloud config set project $PROJECT_NAME +gcloud services list \ No newline at end of file diff --git a/examples/guardrails/folder-factory/main.tf b/examples/guardrails/bitbucket/folder-factory/main.tf similarity index 100% rename from examples/guardrails/folder-factory/main.tf rename to examples/guardrails/bitbucket/folder-factory/main.tf diff --git a/examples/guardrails/folder-factory/modules/folder/README.md b/examples/guardrails/bitbucket/folder-factory/modules/folder/README.md similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/README.md rename to examples/guardrails/bitbucket/folder-factory/modules/folder/README.md diff --git a/examples/guardrails/folder-factory/modules/folder/firewall-policies.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/firewall-policies.tf similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/firewall-policies.tf rename to examples/guardrails/bitbucket/folder-factory/modules/folder/firewall-policies.tf diff --git a/examples/guardrails/folder-factory/modules/folder/iam.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/iam.tf similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/iam.tf rename to examples/guardrails/bitbucket/folder-factory/modules/folder/iam.tf diff --git a/examples/guardrails/folder-factory/modules/folder/logging.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/logging.tf similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/logging.tf rename to examples/guardrails/bitbucket/folder-factory/modules/folder/logging.tf diff --git a/examples/guardrails/folder-factory/modules/folder/main.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/main.tf similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/main.tf rename to examples/guardrails/bitbucket/folder-factory/modules/folder/main.tf diff --git a/examples/guardrails/folder-factory/modules/folder/organization-policies.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/organization-policies.tf similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/organization-policies.tf rename to examples/guardrails/bitbucket/folder-factory/modules/folder/organization-policies.tf diff --git a/examples/guardrails/folder-factory/modules/folder/outputs.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/outputs.tf similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/outputs.tf rename to examples/guardrails/bitbucket/folder-factory/modules/folder/outputs.tf diff --git a/examples/guardrails/folder-factory/modules/folder/tags.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/tags.tf similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/tags.tf rename to examples/guardrails/bitbucket/folder-factory/modules/folder/tags.tf diff --git a/examples/guardrails/folder-factory/modules/folder/variables.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/variables.tf similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/variables.tf rename to examples/guardrails/bitbucket/folder-factory/modules/folder/variables.tf diff --git a/examples/guardrails/folder-factory/modules/folder/versions.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/versions.tf similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/versions.tf rename to examples/guardrails/bitbucket/folder-factory/modules/folder/versions.tf diff --git a/examples/guardrails/folder-factory/outputs.tf b/examples/guardrails/bitbucket/folder-factory/outputs.tf similarity index 100% rename from examples/guardrails/folder-factory/outputs.tf rename to examples/guardrails/bitbucket/folder-factory/outputs.tf diff --git a/examples/guardrails/folder-factory/provider.tf b/examples/guardrails/bitbucket/folder-factory/provider.tf similarity index 100% rename from examples/guardrails/folder-factory/provider.tf rename to examples/guardrails/bitbucket/folder-factory/provider.tf diff --git a/examples/guardrails/folder-factory/variables.tf b/examples/guardrails/bitbucket/folder-factory/variables.tf similarity index 100% rename from examples/guardrails/folder-factory/variables.tf rename to examples/guardrails/bitbucket/folder-factory/variables.tf diff --git a/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml new file mode 100644 index 0000000..cada72f --- /dev/null +++ b/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml @@ -0,0 +1,59 @@ +image: google/cloud-sdk +pipelines: + default: + - step: + name: setup terraform + script: + - apt-get update && apt-get install unzip wget -y + - wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip || echo fine... + - unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip + - yes | cp terraform /usr/bin/ + - ls /usr/bin + artifacts: + - terraform + - step: + oidc: true + name: configure oidc + script: + - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - ./get_oidctoken.sh + artifacts: + - sts-creds.json + - /tmp/gcp_access_token.out + - step: + name: terraform init + oidc: true + script: + - cp terraform /usr/bin/ + - ls . /usr/bin/ + - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - ./get_oidctoken.sh + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json + - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - /usr/bin/terraform plan + branches: + main: + - step: + name: setup terraform + script: + - apt-get update && apt-get install unzip wget -y + - wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip || echo fine... + - unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip + - yes | cp terraform /usr/bin/ + - ls /usr/bin + artifacts: + - terraform + - step: + name: master branch execution tf-apply + oidc: true + script: + - cp terraform /usr/bin/ + - ls . /usr/bin/ + - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - ./get_oidctoken.sh + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json + - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - /usr/bin/terraform plan + - /usr/bin/terraform apply -auto-approve + + diff --git a/examples/guardrails/project-factory/.gitignore b/examples/guardrails/bitbucket/project-factory/.gitignore similarity index 100% rename from examples/guardrails/project-factory/.gitignore rename to examples/guardrails/bitbucket/project-factory/.gitignore diff --git a/examples/guardrails/project-factory/README.md b/examples/guardrails/bitbucket/project-factory/README.md similarity index 100% rename from examples/guardrails/project-factory/README.md rename to examples/guardrails/bitbucket/project-factory/README.md diff --git a/examples/guardrails/project-factory/data/projects/project.yaml.sample b/examples/guardrails/bitbucket/project-factory/data/projects/project.yaml.sample similarity index 100% rename from examples/guardrails/project-factory/data/projects/project.yaml.sample rename to examples/guardrails/bitbucket/project-factory/data/projects/project.yaml.sample diff --git a/examples/guardrails/bitbucket/project-factory/get_oidctoken.sh b/examples/guardrails/bitbucket/project-factory/get_oidctoken.sh new file mode 100755 index 0000000..b032b70 --- /dev/null +++ b/examples/guardrails/bitbucket/project-factory/get_oidctoken.sh @@ -0,0 +1,7 @@ +#!/bin/bash +echo -n "${BITBUCKET_STEP_OIDC_TOKEN}" > /tmp/gcp_access_token.out +gcloud iam workload-identity-pools create-cred-config "projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/${workload_identity_pool_id}/providers/${workload_identity_pool_provider_id}" --credential-source-file=/tmp/gcp_access_token.out --service-account="${SERVICE_ACCOUNT_EMAIL}" --output-file=sts-creds.json +export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json +gcloud auth login --cred-file=`pwd`/sts-creds.json +gcloud config set project $PROJECT_NAME +gcloud services list \ No newline at end of file diff --git a/examples/guardrails/bitbucket/project-factory/main.tf b/examples/guardrails/bitbucket/project-factory/main.tf new file mode 100644 index 0000000..2f0eff9 --- /dev/null +++ b/examples/guardrails/bitbucket/project-factory/main.tf @@ -0,0 +1,35 @@ +/** + * 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 + * + * 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 { + projects = { + for f in fileset("./data/projects", "**/*.yaml") : + trimsuffix(f, ".yaml") => yamldecode(file("./data/projects/${f}")) + } +} + +module "project" { + source = "./modules/project_plus" + for_each = local.projects + team = each.key + repo_sub = each.value.repo_branch + repo_provider = each.value.repo_provider + billing_account = each.value.billing_account_id + folder = var.folder + roles = try(each.value.roles, []) + wif-pool = google_iam_workload_identity_pool.wif-pool-bitbucket.name + depends_on = [google_iam_workload_identity_pool.wif-pool-bitbucket] +} diff --git a/examples/guardrails/project-factory/modules/project/README.md b/examples/guardrails/bitbucket/project-factory/modules/project/README.md similarity index 100% rename from examples/guardrails/project-factory/modules/project/README.md rename to examples/guardrails/bitbucket/project-factory/modules/project/README.md diff --git a/examples/guardrails/project-factory/modules/project/iam.tf b/examples/guardrails/bitbucket/project-factory/modules/project/iam.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/iam.tf rename to examples/guardrails/bitbucket/project-factory/modules/project/iam.tf diff --git a/examples/guardrails/project-factory/modules/project/logging.tf b/examples/guardrails/bitbucket/project-factory/modules/project/logging.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/logging.tf rename to examples/guardrails/bitbucket/project-factory/modules/project/logging.tf diff --git a/examples/guardrails/project-factory/modules/project/main.tf b/examples/guardrails/bitbucket/project-factory/modules/project/main.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/main.tf rename to examples/guardrails/bitbucket/project-factory/modules/project/main.tf diff --git a/examples/guardrails/project-factory/modules/project/organization-policies.tf b/examples/guardrails/bitbucket/project-factory/modules/project/organization-policies.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/organization-policies.tf rename to examples/guardrails/bitbucket/project-factory/modules/project/organization-policies.tf diff --git a/examples/guardrails/project-factory/modules/project/outputs.tf b/examples/guardrails/bitbucket/project-factory/modules/project/outputs.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/outputs.tf rename to examples/guardrails/bitbucket/project-factory/modules/project/outputs.tf diff --git a/examples/guardrails/project-factory/modules/project/service-accounts.tf b/examples/guardrails/bitbucket/project-factory/modules/project/service-accounts.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/service-accounts.tf rename to examples/guardrails/bitbucket/project-factory/modules/project/service-accounts.tf diff --git a/examples/guardrails/project-factory/modules/project/shared-vpc.tf b/examples/guardrails/bitbucket/project-factory/modules/project/shared-vpc.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/shared-vpc.tf rename to examples/guardrails/bitbucket/project-factory/modules/project/shared-vpc.tf diff --git a/examples/guardrails/project-factory/modules/project/tags.tf b/examples/guardrails/bitbucket/project-factory/modules/project/tags.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/tags.tf rename to examples/guardrails/bitbucket/project-factory/modules/project/tags.tf diff --git a/examples/guardrails/project-factory/modules/project/variables.tf b/examples/guardrails/bitbucket/project-factory/modules/project/variables.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/variables.tf rename to examples/guardrails/bitbucket/project-factory/modules/project/variables.tf diff --git a/examples/guardrails/project-factory/modules/project/versions.tf b/examples/guardrails/bitbucket/project-factory/modules/project/versions.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/versions.tf rename to examples/guardrails/bitbucket/project-factory/modules/project/versions.tf diff --git a/examples/guardrails/project-factory/modules/project/vpc-sc.tf b/examples/guardrails/bitbucket/project-factory/modules/project/vpc-sc.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/vpc-sc.tf rename to examples/guardrails/bitbucket/project-factory/modules/project/vpc-sc.tf diff --git a/examples/guardrails/project-factory/modules/project_plus/README.md b/examples/guardrails/bitbucket/project-factory/modules/project_plus/README.md similarity index 100% rename from examples/guardrails/project-factory/modules/project_plus/README.md rename to examples/guardrails/bitbucket/project-factory/modules/project_plus/README.md diff --git a/examples/guardrails/bitbucket/project-factory/modules/project_plus/main.tf b/examples/guardrails/bitbucket/project-factory/modules/project_plus/main.tf new file mode 100644 index 0000000..2332f16 --- /dev/null +++ b/examples/guardrails/bitbucket/project-factory/modules/project_plus/main.tf @@ -0,0 +1,46 @@ +/** + * 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 + * + * 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. + */ + +resource "random_id" "rand" { + byte_length = 4 +} + +module "project" { + source = "./../project" + name = "${var.team}-prj-${random_id.rand.hex}" + parent = var.folder + billing_account = var.billing_account +} + +resource "google_service_account" "sa" { + account_id = "${var.team}-sa-${random_id.rand.hex}" + display_name = "Service account ${var.team}" + project = module.project.project_id +} + +resource "google_service_account_iam_member" "sa-iam" { + service_account_id = google_service_account.sa.name + role = "roles/iam.workloadIdentityUser" + member = "principalSet://iam.googleapis.com/${var.wif-pool}/attribute.branch_name/${var.repo_sub}" +} + +resource "google_project_iam_member" "sa-project" { + for_each = toset(var.roles) + role = each.value + member = "serviceAccount:${google_service_account.sa.email}" + project = module.project.project_id + depends_on = [google_service_account.sa] +} diff --git a/examples/guardrails/project-factory/modules/project_plus/outputs.tf b/examples/guardrails/bitbucket/project-factory/modules/project_plus/outputs.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project_plus/outputs.tf rename to examples/guardrails/bitbucket/project-factory/modules/project_plus/outputs.tf diff --git a/examples/guardrails/project-factory/modules/project_plus/variables.tf b/examples/guardrails/bitbucket/project-factory/modules/project_plus/variables.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project_plus/variables.tf rename to examples/guardrails/bitbucket/project-factory/modules/project_plus/variables.tf diff --git a/examples/guardrails/project-factory/modules/project_plus/versions.tf b/examples/guardrails/bitbucket/project-factory/modules/project_plus/versions.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project_plus/versions.tf rename to examples/guardrails/bitbucket/project-factory/modules/project_plus/versions.tf diff --git a/examples/guardrails/bitbucket/project-factory/outputs.tf b/examples/guardrails/bitbucket/project-factory/outputs.tf new file mode 100644 index 0000000..5261bb9 --- /dev/null +++ b/examples/guardrails/bitbucket/project-factory/outputs.tf @@ -0,0 +1,31 @@ +/** + * 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 + * + * 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. + */ + +output "wif_pool_id_bitbucket" { + description = "Bitbucket Workload Identity Pool" + value = google_iam_workload_identity_pool.wif-pool-bitbucket.name +} + +output "wif_provider_id_bitbucket" { + description = "Gitlab Workload Identity Pool" + value = google_iam_workload_identity_pool_provider.wif-provider-bitbucket.name +} + + +output "projects" { + description = "Created projects and service accounts." + value = module.project +} \ No newline at end of file diff --git a/examples/guardrails/project-factory/provider.tf b/examples/guardrails/bitbucket/project-factory/provider.tf similarity index 100% rename from examples/guardrails/project-factory/provider.tf rename to examples/guardrails/bitbucket/project-factory/provider.tf diff --git a/examples/guardrails/bitbucket/project-factory/variables.tf b/examples/guardrails/bitbucket/project-factory/variables.tf new file mode 100644 index 0000000..565e093 --- /dev/null +++ b/examples/guardrails/bitbucket/project-factory/variables.tf @@ -0,0 +1,27 @@ +/** + * 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 + * + * 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. + */ + +variable "folder" { +} + +variable "billing_account" { +} +variable "issuer_uri" { +} + +variable "allowed_audiences" { + type = list +} \ No newline at end of file diff --git a/examples/guardrails/bitbucket/project-factory/wif.tf b/examples/guardrails/bitbucket/project-factory/wif.tf new file mode 100644 index 0000000..092ce34 --- /dev/null +++ b/examples/guardrails/bitbucket/project-factory/wif.tf @@ -0,0 +1,50 @@ +/** + * 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 + * + * 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. + */ + +resource "random_id" "rand" { + byte_length = 4 +} + +module "wif-project" { + source = "./modules/project" + name = "wif-prj-${random_id.rand.hex}" + parent = var.folder + billing_account = var.billing_account +} +resource "google_iam_workload_identity_pool" "wif-pool-bitbucket" { + provider = google-beta + workload_identity_pool_id = "bitbucket-pool1-${random_id.rand.hex}" + project = module.wif-project.project_id +} +resource "google_iam_workload_identity_pool_provider" "wif-provider-bitbucket" { + provider = google-beta + workload_identity_pool_id = google_iam_workload_identity_pool.wif-pool-bitbucket.workload_identity_pool_id + workload_identity_pool_provider_id = "bitbucket-provider-${random_id.rand.hex}" + project = module.wif-project.project_id + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.sub" = "assertion.sub" + "attribute.deployment_environment_uuid" = "assertion.deploymentEnvironmentUuid" + "attribute.branch_name" = "assertion.branchName" + } + oidc { + issuer_uri = var.issuer_uri + allowed_audiences = var.allowed_audiences + } + depends_on = [ + google_iam_workload_identity_pool.wif-pool-bitbucket + ] +} diff --git a/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml new file mode 100644 index 0000000..ef8f334 --- /dev/null +++ b/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml @@ -0,0 +1,99 @@ +image: google/cloud-sdk +pipelines: + default: + - step: + name: setup terraform + script: + - apt-get update && apt-get install unzip wget -y + - wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip || echo fine... + - unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip + - yes | cp terraform /usr/bin/ + - ls /usr/bin + artifacts: + - terraform + - step: + name: terraform init + oidc: true + script: + - cp terraform /usr/bin/ + - ls . /usr/bin/ + - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - ./get_oidctoken.sh + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json + - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - /usr/bin/terraform plan + branches: + staging: + - step: + name: setup terraform + script: + - apt-get update && apt-get install unzip wget -y + - wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip || echo fine... + - unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip + - yes | cp terraform /usr/bin/ + - ls /usr/bin + artifacts: + - terraform + - step: + name: master branch execution tf-apply + oidc: true + script: + - cp terraform /usr/bin/ + - ls . /usr/bin/ + - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - echo $GCP_PROJECT_NUMBER ${workload_identity_pool_id} ${workload_identity_pool_provider_id} ${SERVICE_ACCOUNT_EMAIL} $PROJECT_NAME + - ./get_oidctoken.sh + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json + - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - /usr/bin/terraform plan + - /usr/bin/terraform apply -auto-approve + dev: + - step: + name: setup terraform + script: + - apt-get update && apt-get install unzip wget -y + - wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip || echo fine... + - unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip + - yes | cp terraform /usr/bin/ + - ls /usr/bin + artifacts: + - terraform + - step: + name: dev branch execution tf-apply + oidc: true + script: + - cp terraform /usr/bin/ + - ls . /usr/bin/ + - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - echo $GCP_PROJECT_NUMBER ${workload_identity_pool_id} ${workload_identity_pool_provider_id} ${SERVICE_ACCOUNT_EMAIL} $PROJECT_NAME + - ./get_oidctoken.sh + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json + - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - /usr/bin/terraform plan + - /usr/bin/terraform apply -auto-approve + prod: + - step: + name: setup terraform + script: + - apt-get update && apt-get install unzip wget -y + - wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip || echo fine... + - unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip + - yes | cp terraform /usr/bin/ + - ls /usr/bin + artifacts: + - terraform + - step: + name: prod branch execution tf-apply + oidc: true + script: + - cp terraform /usr/bin/ + - ls . /usr/bin/ + - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - echo $GCP_PROJECT_NUMBER ${workload_identity_pool_id} ${workload_identity_pool_provider_id} ${SERVICE_ACCOUNT_EMAIL} $PROJECT_NAME + - ./get_oidctoken.sh + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json + - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - /usr/bin/terraform plan + - /usr/bin/terraform apply -auto-approve + + diff --git a/examples/guardrails/bitbucket/skunkworks/.gitignore b/examples/guardrails/bitbucket/skunkworks/.gitignore new file mode 100644 index 0000000..a5d8c0e --- /dev/null +++ b/examples/guardrails/bitbucket/skunkworks/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.terraform +.terraform.lock.hcl diff --git a/examples/guardrails/skunkworks/README.md b/examples/guardrails/bitbucket/skunkworks/README.md similarity index 100% rename from examples/guardrails/skunkworks/README.md rename to examples/guardrails/bitbucket/skunkworks/README.md diff --git a/examples/guardrails/bitbucket/skunkworks/get_oidctoken.sh b/examples/guardrails/bitbucket/skunkworks/get_oidctoken.sh new file mode 100755 index 0000000..b032b70 --- /dev/null +++ b/examples/guardrails/bitbucket/skunkworks/get_oidctoken.sh @@ -0,0 +1,7 @@ +#!/bin/bash +echo -n "${BITBUCKET_STEP_OIDC_TOKEN}" > /tmp/gcp_access_token.out +gcloud iam workload-identity-pools create-cred-config "projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/${workload_identity_pool_id}/providers/${workload_identity_pool_provider_id}" --credential-source-file=/tmp/gcp_access_token.out --service-account="${SERVICE_ACCOUNT_EMAIL}" --output-file=sts-creds.json +export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json +gcloud auth login --cred-file=`pwd`/sts-creds.json +gcloud config set project $PROJECT_NAME +gcloud services list \ No newline at end of file diff --git a/examples/guardrails/bitbucket/skunkworks/main.tf b/examples/guardrails/bitbucket/skunkworks/main.tf new file mode 100644 index 0000000..386a436 --- /dev/null +++ b/examples/guardrails/bitbucket/skunkworks/main.tf @@ -0,0 +1,23 @@ +/** + * 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 + * + * 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. + */ + +resource "google_storage_bucket" "bucket" { + project = var.project + name = lower("${var.project}-test-bucket") + location = "EU" + force_destroy = true +} + diff --git a/examples/guardrails/skunkworks/provider.tf b/examples/guardrails/bitbucket/skunkworks/provider.tf similarity index 100% rename from examples/guardrails/skunkworks/provider.tf rename to examples/guardrails/bitbucket/skunkworks/provider.tf diff --git a/examples/guardrails/bitbucket/skunkworks/variables.tf b/examples/guardrails/bitbucket/skunkworks/variables.tf new file mode 100644 index 0000000..2fbadd4 --- /dev/null +++ b/examples/guardrails/bitbucket/skunkworks/variables.tf @@ -0,0 +1,19 @@ +/** + * 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 + * + * 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. + */ + +variable "project" { + type = string +} diff --git a/examples/guardrails/cloudbuild/folder-factory/.cloudbuild/workflows/README.md b/examples/guardrails/cloudbuild/folder-factory/.cloudbuild/workflows/README.md new file mode 100644 index 0000000..6473211 --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/.cloudbuild/workflows/README.md @@ -0,0 +1,43 @@ +# Build Repository from Gitlab + +Gitlab repository can be build with Cloud build by webhook triggers. This document covers the steps to setup webhooks triggers to build repository from gitlab: + +## Prerequisites + +* Enable the Cloud Build and secret manager API +* [Enable ssh access on gitlab](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#enabling_ssh_access_on_gitlab). + + +## Setup SSH Keys + +1. To access the gitlab code, ssh keys needs to be retrieved in the inline build config. +2. Follow the steps in [this](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#creating_an_ssh_key) document to create ssh keys. +3. [Add your public ssh access keys on gitlab](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#adding_your_public_ssh_access_key_on_gitlab). +4. [Add ssh key credentials in Secret Manager](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#webhook_triggers_create_store_secret). +5. [Create a webhook trigger from the GCP console](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#creating_webhook_triggers). +6. [Create a webhook in gitlab](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#creating_a_webhook_in_gitlab) using the webhook URL generated in step #5. + + +## Update variable substitutions in webhook trigger + + +Following substitutions variables needs to be configured in cloud build trigger settings to complete cloud build trigger setup. + +variables: +``` + _BRANCH: $(body.ref) + # This variable provides information about the branch which invoked the trigger. Substitute its value with $(body.ref) + _SECRET: 'projects//secrets//versions/' + # It contains the URI of a secret manager resource with an ssh key which has access to the target gitlab repository. E.g. projects/123456789/secrets/gitlab-ssh/versions/1 + _FOLDER: 'XXXX' + # It contains the directory with terraform configuration files which needs to be build when the trigger is invoked. E.g. examples/guardrails/skunkworks + _REPOSITORY_NAME: $(body.repository.name) + # It contains the name of the repository. Substitute its value with $(body.repository.name) + _STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + _TO_SHA: $(body.after) + # It contains the SHA of the commit that invoked the build. Substitute its value with $(body.after) + _SSH_REPOSITORY_NAME: 'XXXX' + # It contains the gitlab URL of the repository in SSH format. E.g. git@gitlab.com:gitlab-org/cloud-build-terraform-iac.git +``` + diff --git a/examples/guardrails/cloudbuild/folder-factory/.cloudbuild/workflows/cloudbuild.yaml b/examples/guardrails/cloudbuild/folder-factory/.cloudbuild/workflows/cloudbuild.yaml new file mode 100644 index 0000000..af97872 --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/.cloudbuild/workflows/cloudbuild.yaml @@ -0,0 +1,93 @@ +# 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 +# +# 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. + +steps: +# Setup SSH: +# 1- save the SSH key from Secret Manager to a file +# 2- add the host key to the known_hosts file + - name: gcr.io/cloud-builders/git + args: + - '-c' + - | + echo "$$SSHKEY" > /root/.ssh/id_rsa + chmod 400 /root/.ssh/id_rsa + ssh-keyscan gitlab.com > /root/.ssh/known_hosts + entrypoint: bash + secretEnv: + - SSHKEY + volumes: + - name: ssh + path: /root/.ssh + + # Clone the repository + - name: gcr.io/cloud-builders/git + args: + - clone + - '-n' + - $_SSH_REPOSITORY_NAME + - . + volumes: + - name: ssh + path: /root/.ssh + + # Checkout the specific commit that invoked this build + - name: gcr.io/cloud-builders/git + args: + - checkout + - $_TO_SHA + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - id: 'Terraform Init' + name: 'hashicorp/terraform' + dir: $_FOLDER + entrypoint: 'sh' + args: + - '-c' + - | + terraform init \ + -backend-config="bucket=$_STATE_BUCKET" \ + -backend-config="prefix=$_REPOSITORY_NAME" \ + + # Generates an execution plan for Terraform + - id: ' Terraform Plan' + name: 'hashicorp/terraform' + dir: $_FOLDER + entrypoint: 'sh' + args: + - '-c' + - | + terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + - id: 'Terraform Apply' + name: 'hashicorp/terraform' + dir: $_FOLDER + entrypoint: 'sh' + args: + - '-c' + - | + if [ "$_BRANCH" == "refs/heads/main" ];then + terraform apply -auto-approve + fi + +# This field is used for secret from Secret Manager with Cloud Build. +availableSecrets: + secretManager: + - versionName: $_SECRET + env: SSHKEY + +# Use this option to specify the logs location. With CLOUD_LOGGING_ONLY, logs are stored in Cloud Logging. See the document for more logging options https://cloud.google.com/build/docs/api/reference/rest/v1/projects.builds#loggingmode. +options: + logging: CLOUD_LOGGING_ONLY + \ No newline at end of file diff --git a/examples/guardrails/folder-factory/.github/workflows/terraform-deployment.yml b/examples/guardrails/cloudbuild/folder-factory/.github/workflows/terraform-deployment.yml similarity index 100% rename from examples/guardrails/folder-factory/.github/workflows/terraform-deployment.yml rename to examples/guardrails/cloudbuild/folder-factory/.github/workflows/terraform-deployment.yml diff --git a/examples/guardrails/cloudbuild/folder-factory/.gitignore b/examples/guardrails/cloudbuild/folder-factory/.gitignore new file mode 100644 index 0000000..a5d8c0e --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.terraform +.terraform.lock.hcl diff --git a/examples/guardrails/folder-factory/.cloudbuild/workflows/cloudbuild.yaml b/examples/guardrails/cloudbuild/folder-factory/.gitlab/workflows/cloudbuild.yaml similarity index 100% rename from examples/guardrails/folder-factory/.cloudbuild/workflows/cloudbuild.yaml rename to examples/guardrails/cloudbuild/folder-factory/.gitlab/workflows/cloudbuild.yaml diff --git a/examples/guardrails/folder-factory/.gitlab/workflows/cloudbuild.yaml b/examples/guardrails/cloudbuild/folder-factory/.jenkins/workflows/cloudbuild.yaml similarity index 100% rename from examples/guardrails/folder-factory/.gitlab/workflows/cloudbuild.yaml rename to examples/guardrails/cloudbuild/folder-factory/.jenkins/workflows/cloudbuild.yaml diff --git a/examples/guardrails/folder-factory/.jenkins/workflows/cloudbuild.yaml b/examples/guardrails/cloudbuild/folder-factory/.terraform-cloud/workflows/cloudbuild.yaml similarity index 100% rename from examples/guardrails/folder-factory/.jenkins/workflows/cloudbuild.yaml rename to examples/guardrails/cloudbuild/folder-factory/.terraform-cloud/workflows/cloudbuild.yaml diff --git a/examples/guardrails/folder-factory/.terraform-cloud/workflows/cloudbuild.yaml b/examples/guardrails/cloudbuild/folder-factory/.terraform-enterprise/worflows/cloudbuild.yaml similarity index 100% rename from examples/guardrails/folder-factory/.terraform-cloud/workflows/cloudbuild.yaml rename to examples/guardrails/cloudbuild/folder-factory/.terraform-enterprise/worflows/cloudbuild.yaml diff --git a/examples/guardrails/cloudbuild/folder-factory/README.md b/examples/guardrails/cloudbuild/folder-factory/README.md new file mode 100644 index 0000000..a139948 --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/README.md @@ -0,0 +1,59 @@ +# Folder Factory + +This is a template for a DevOps folder factory. + +It can be used with [https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory](https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory) and is intended to house the folder configurations: + +![Screenshot 2022-05-10 12 00 19 PM](https://user-images.githubusercontent.com/94000358/169809437-aaa8538e-3ffc-48b3-9028-84e4995de150.png) + +Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: + +Secret configuration + +or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: +``` +env: + STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' + # The workload identity provider that should be used for this repository. + SERVICE_ACCOUNT: 'XXXX@XXXX' + # The service account that should be used for this repository. +``` + +## Setting up folders + +The folder factory will: +- create a folders with defined organisational policies + +It uses YAML configuration files for every folder with the following sample structure: +``` +parent: folders/XXXXXXXXX +org_policies: + policy_boolean: + constraints/compute.disableGuestAttributesAccess: true + constraints/iam.disableServiceAccountCreation: false + constraints/iam.disableServiceAccountKeyCreation: false + constraints/iam.disableServiceAccountKeyUpload: false + constraints/gcp.disableCloudLogging: false + policy_list: + constraints/compute.vmExternalIpAccess: + inherit_from_parent: null + status: true + suggested_value: null + values: +iam: + roles/resourcemanager.projectCreator: + - serviceAccount:XXXXX@XXXXXX +``` + +Every folder is defined with its own yaml file located in the following [Folder](data/folder). diff --git a/examples/guardrails/cloudbuild/folder-factory/data/folders/folder.yaml.sample b/examples/guardrails/cloudbuild/folder-factory/data/folders/folder.yaml.sample new file mode 100644 index 0000000..9ea745d --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/data/folders/folder.yaml.sample @@ -0,0 +1,31 @@ +# 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 +# +# 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. + +parent: folders/01234567890 +org_policies: + policy_boolean: + constraints/compute.disableGuestAttributesAccess: true + constraints/iam.disableServiceAccountCreation: false + constraints/iam.disableServiceAccountKeyCreation: false + constraints/iam.disableServiceAccountKeyUpload: false + constraints/gcp.disableCloudLogging: false + policy_list: + constraints/compute.vmExternalIpAccess: + inherit_from_parent: null + status: true + suggested_value: null + values: +iam: + roles/resourcemanager.projectCreator: + - serviceAccount:service-account@project-xyz.iam.gserviceaccount.com \ No newline at end of file diff --git a/examples/guardrails/cloudbuild/folder-factory/main.tf b/examples/guardrails/cloudbuild/folder-factory/main.tf new file mode 100644 index 0000000..d024c17 --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/main.tf @@ -0,0 +1,32 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + folders = { + for f in fileset("./data/folders", "**/*.yaml") : + trimsuffix(f, ".yaml") => yamldecode(file("./data/folders/${f}")) + } +} + +module "folder" { + source = "./modules/folder" + for_each = local.folders + name = each.key + parent = each.value.parent + policy_boolean = try(each.value.org_policies.policy_boolean, {}) + policy_list = try(each.value.org_policies.policy_list, {}) + iam = try(each.value.iam, {}) +} \ No newline at end of file diff --git a/examples/guardrails/cloudbuild/folder-factory/modules/folder/README.md b/examples/guardrails/cloudbuild/folder-factory/modules/folder/README.md new file mode 100644 index 0000000..4f6898b --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/modules/folder/README.md @@ -0,0 +1,299 @@ +# Google Cloud Folder Module + +This module allows the creation and management of folders, including support for IAM bindings, organization policies, and hierarchical firewall rules. + +## Examples + +### IAM bindings + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + group_iam = { + "cloud-owners@example.org" = [ + "roles/owner", + "roles/resourcemanager.projectCreator" + ] + } + iam = { + "roles/owner" = ["user:one@example.com"] + } +} +# tftest modules=1 resources=3 +``` + +### Organization policies + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + policy_boolean = { + "constraints/compute.disableGuestAttributesAccess" = true + "constraints/compute.skipDefaultNetworkCreation" = true + } + policy_list = { + "constraints/compute.trustedImageProjects" = { + inherit_from_parent = null + suggested_value = null + status = true + values = ["projects/my-project"] + } + } +} +# tftest modules=1 resources=4 +``` + +### Firewall policy factory + +In the same way as for the [organization](../organization) module, the in-built factory allows you to define a single policy, using one file for rules, and an optional file for CIDR range substitution variables. Remember that non-absolute paths are relative to the root module (the folder where you run `terraform`). + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + firewall_policy_factory = { + cidr_file = "data/cidrs.yaml" + policy_name = null + rules_file = "data/rules.yaml" + } + firewall_policy_association = { + factory-policy = module.folder.firewall_policy_id["factory"] + } +} +# tftest skip +``` + +```yaml +# cidrs.yaml + +rfc1918: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 +``` + +```yaml +# rules.yaml + +allow-admins: + description: Access from the admin subnet to all subnets + direction: INGRESS + action: allow + priority: 1000 + ranges: + - $rfc1918 + ports: + all: [] + target_resources: null + enable_logging: false + +allow-ssh-from-iap: + description: Enable SSH from IAP + direction: INGRESS + action: allow + priority: 1002 + ranges: + - 35.235.240.0/20 + ports: + tcp: ["22"] + target_resources: null + enable_logging: false +``` + +### Logging Sinks + +```hcl +module "gcs" { + source = "./modules/gcs" + project_id = "my-project" + name = "gcs_sink" + force_destroy = true +} + +module "dataset" { + source = "./modules/bigquery-dataset" + project_id = "my-project" + id = "bq_sink" +} + +module "pubsub" { + source = "./modules/pubsub" + project_id = "my-project" + name = "pubsub_sink" +} + +module "bucket" { + source = "./modules/logging-bucket" + parent_type = "project" + parent = "my-project" + id = "bucket" +} + +module "folder-sink" { + source = "./modules/folder" + parent = "folders/657104291943" + name = "my-folder" + logging_sinks = { + warnings = { + type = "storage" + destination = module.gcs.name + filter = "severity=WARNING" + include_children = true + exclusions = {} + } + info = { + type = "bigquery" + destination = module.dataset.id + filter = "severity=INFO" + include_children = true + exclusions = {} + } + notice = { + type = "pubsub" + destination = module.pubsub.id + filter = "severity=NOTICE" + include_children = true + exclusions = {} + } + debug = { + type = "logging" + destination = module.bucket.id + filter = "severity=DEBUG" + include_children = true + exclusions = { + no-compute = "logName:compute" + } + } + } + logging_exclusions = { + no-gce-instances = "resource.type=gce_instance" + } +} +# tftest modules=5 resources=14 +``` + +### Hierarchical firewall policies + +```hcl +module "folder1" { + source = "./modules/folder" + parent = var.organization_id + name = "policy-container" + + firewall_policies = { + iap-policy = { + allow-iap-ssh = { + description = "Always allow ssh from IAP" + direction = "INGRESS" + action = "allow" + priority = 100 + ranges = ["35.235.240.0/20"] + ports = { tcp = ["22"] } + target_service_accounts = null + target_resources = null + logging = false + } + } + } + firewall_policy_association = { + iap-policy = "iap-policy" + } +} + +module "folder2" { + source = "./modules/folder" + parent = var.organization_id + name = "hf2" + firewall_policy_association = { + iap-policy = module.folder1.firewall_policy_id["iap-policy"] + } +} +# tftest modules=2 resources=6 +``` + +## 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. + +```hcl +module "org" { + source = "./modules/organization" + organization_id = var.organization_id + tags = { + environment = { + description = "Environment specification." + iam = null + values = { + dev = null + prod = null + } + } + } +} + +module "folder" { + source = "./modules/folder" + name = "Test" + parent = module.org.organization_id + tag_bindings = { + env-prod = module.org.tag_values["environment/prod"].id + foo = "tagValues/12345678" + } +} +# tftest modules=2 resources=6 +``` + + + + +## Files + +| name | description | resources | +|---|---|---| +| [firewall-policies.tf](./firewall-policies.tf) | None | google_compute_firewall_policy · google_compute_firewall_policy_association · google_compute_firewall_policy_rule | +| [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_folder_iam_binding | +| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_folder_exclusion · google_logging_folder_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | +| [main.tf](./main.tf) | Module-level locals and resources. | google_essential_contacts_contact · google_folder | +| [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_folder_organization_policy | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding | +| [variables.tf](./variables.tf) | Module variables. | | +| [versions.tf](./versions.tf) | Version pins. | | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | +| [firewall_policies](variables.tf#L24) | Hierarchical firewall policies created in this folder. | map(map(object({…}))) | | {} | +| [firewall_policy_association](variables.tf#L41) | The hierarchical firewall policy to associate to this folder. Must be either a key in the `firewall_policies` map or the id of a policy defined somewhere else. | map(string) | | {} | +| [firewall_policy_factory](variables.tf#L48) | Configuration for the firewall policy factory. | object({…}) | | null | +| [folder_create](variables.tf#L58) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | +| [group_iam](variables.tf#L64) | 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. | map(list(string)) | | {} | +| [iam](variables.tf#L71) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [id](variables.tf#L78) | Folder ID in case you use folder_create=false. | string | | null | +| [logging_exclusions](variables.tf#L84) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L91) | Logging sinks to create for this folder. | map(object({…})) | | {} | +| [name](variables.tf#L112) | Folder name. | string | | null | +| [parent](variables.tf#L118) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [policy_boolean](variables.tf#L128) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | +| [policy_list](variables.tf#L135) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L147) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [firewall_policies](outputs.tf#L16) | Map of firewall policy resources created in this folder. | | +| [firewall_policy_id](outputs.tf#L21) | Map of firewall policy ids created in this folder. | | +| [folder](outputs.tf#L26) | Folder resource. | | +| [id](outputs.tf#L31) | Folder id. | | +| [name](outputs.tf#L41) | Folder name. | | +| [sink_writer_identities](outputs.tf#L46) | Writer identities created for each sink. | | + + diff --git a/examples/guardrails/cloudbuild/folder-factory/modules/folder/firewall-policies.tf b/examples/guardrails/cloudbuild/folder-factory/modules/folder/firewall-policies.tf new file mode 100644 index 0000000..96224c5 --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/modules/folder/firewall-policies.tf @@ -0,0 +1,93 @@ +/** + * 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 + * + * 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 { + _factory_cidrs = try( + yamldecode(file(var.firewall_policy_factory.cidr_file)), {} + ) + _factory_name = ( + try(var.firewall_policy_factory.policy_name, null) == null + ? "factory" + : var.firewall_policy_factory.policy_name + ) + _factory_rules = try( + yamldecode(file(var.firewall_policy_factory.rules_file)), {} + ) + _factory_rules_parsed = { + for name, rule in local._factory_rules : name => merge(rule, { + ranges = flatten([ + for r in(rule.ranges == null ? [] : rule.ranges) : + lookup(local._factory_cidrs, trimprefix(r, "$"), r) + ]) + }) + } + _merged_rules = flatten([ + for policy, rules in local.firewall_policies : [ + for name, rule in rules : merge(rule, { + policy = policy + name = name + }) + ] + ]) + firewall_policies = merge(var.firewall_policies, ( + length(local._factory_rules) == 0 + ? {} + : { (local._factory_name) = local._factory_rules_parsed } + )) + firewall_rules = { + for r in local._merged_rules : "${r.policy}-${r.name}" => r + } +} + +resource "google_compute_firewall_policy" "policy" { + for_each = local.firewall_policies + short_name = each.key + parent = local.folder.id +} + +resource "google_compute_firewall_policy_rule" "rule" { + for_each = local.firewall_rules + firewall_policy = google_compute_firewall_policy.policy[each.value.policy].id + action = each.value.action + direction = each.value.direction + priority = try(each.value.priority, null) + target_resources = try(each.value.target_resources, null) + target_service_accounts = try(each.value.target_service_accounts, null) + enable_logging = try(each.value.logging, null) + # preview = each.value.preview + description = each.value.description + match { + src_ip_ranges = each.value.direction == "INGRESS" ? each.value.ranges : null + dest_ip_ranges = each.value.direction == "EGRESS" ? each.value.ranges : null + dynamic "layer4_configs" { + for_each = each.value.ports + iterator = port + content { + ip_protocol = port.key + ports = port.value + } + } + } +} + + +resource "google_compute_firewall_policy_association" "association" { + for_each = var.firewall_policy_association + name = replace(local.folder.id, "/", "-") + attachment_target = local.folder.id + firewall_policy = try(google_compute_firewall_policy.policy[each.value].id, each.value) +} + diff --git a/examples/guardrails/cloudbuild/folder-factory/modules/folder/iam.tf b/examples/guardrails/cloudbuild/folder-factory/modules/folder/iam.tf new file mode 100644 index 0000000..52886ba --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/modules/folder/iam.tf @@ -0,0 +1,40 @@ +/** + * 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 + * + * 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 IAM bindings, roles and audit logging resources. + +locals { + group_iam_roles = distinct(flatten(values(var.group_iam))) + group_iam = { + for r in local.group_iam_roles : r => [ + for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null + ] + } + iam = { + for role in distinct(concat(keys(var.iam), keys(local.group_iam))) : + role => concat( + try(var.iam[role], []), + try(local.group_iam[role], []) + ) + } +} + +resource "google_folder_iam_binding" "authoritative" { + for_each = local.iam + folder = local.folder.name + role = each.key + members = each.value +} diff --git a/examples/guardrails/cloudbuild/folder-factory/modules/folder/logging.tf b/examples/guardrails/cloudbuild/folder-factory/modules/folder/logging.tf new file mode 100644 index 0000000..d6a195e --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/modules/folder/logging.tf @@ -0,0 +1,91 @@ +/** + * 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 + * + * 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 Log sinks and supporting resources. + +locals { + sink_bindings = { + for type in ["bigquery", "pubsub", "logging", "storage"] : + type => { + for name, sink in var.logging_sinks : + name => sink + if sink.type == type + } + } +} + +resource "google_logging_folder_sink" "sink" { + for_each = var.logging_sinks + name = each.key + #description = "${each.key} (Terraform-managed)." + folder = local.folder.name + destination = "${each.value.type}.googleapis.com/${each.value.destination}" + filter = each.value.filter + include_children = each.value.include_children + + dynamic "exclusions" { + for_each = each.value.exclusions + iterator = exclusion + content { + name = exclusion.key + filter = exclusion.value + } + } + + depends_on = [ + google_folder_iam_binding.authoritative + ] +} + +resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { + for_each = local.sink_bindings["storage"] + bucket = each.value.destination + role = "roles/storage.objectCreator" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" { + for_each = local.sink_bindings["bigquery"] + project = split("/", each.value.destination)[1] + dataset_id = split("/", each.value.destination)[3] + role = "roles/bigquery.dataEditor" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" { + for_each = local.sink_bindings["pubsub"] + project = split("/", each.value.destination)[1] + topic = split("/", each.value.destination)[3] + role = "roles/pubsub.publisher" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_project_iam_member" "bucket-sinks-binding" { + for_each = local.sink_bindings["logging"] + project = split("/", each.value.destination)[1] + role = "roles/logging.bucketWriter" + member = google_logging_folder_sink.sink[each.key].writer_identity + # TODO(jccb): use a condition to limit writer-identity only to this + # bucket +} + +resource "google_logging_folder_exclusion" "logging-exclusion" { + for_each = var.logging_exclusions + name = each.key + folder = local.folder.name + description = "${each.key} (Terraform-managed)." + filter = each.value +} diff --git a/examples/guardrails/cloudbuild/folder-factory/modules/folder/main.tf b/examples/guardrails/cloudbuild/folder-factory/modules/folder/main.tf new file mode 100644 index 0000000..5d285d2 --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/modules/folder/main.tf @@ -0,0 +1,43 @@ +/** + * 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 + * + * 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 { + folder = ( + var.folder_create + ? try(google_folder.folder.0, null) + : try(data.google_folder.folder.0, null) + ) +} + +data "google_folder" "folder" { + count = var.folder_create ? 0 : 1 + folder = var.id +} + +resource "google_folder" "folder" { + count = var.folder_create ? 1 : 0 + display_name = var.name + parent = var.parent +} + +resource "google_essential_contacts_contact" "contact" { + provider = google-beta + for_each = var.contacts + parent = local.folder.name + email = each.key + language_tag = "en" + notification_category_subscriptions = each.value +} diff --git a/examples/guardrails/cloudbuild/folder-factory/modules/folder/organization-policies.tf b/examples/guardrails/cloudbuild/folder-factory/modules/folder/organization-policies.tf new file mode 100644 index 0000000..177a3d8 --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/modules/folder/organization-policies.tf @@ -0,0 +1,90 @@ +/** + * 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 + * + * 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 Folder-level organization policies. + +resource "google_folder_organization_policy" "boolean" { + for_each = var.policy_boolean + folder = local.folder.name + constraint = each.key + + dynamic "boolean_policy" { + for_each = each.value == null ? [] : [each.value] + iterator = policy + content { + enforced = policy.value + } + } + + dynamic "restore_policy" { + for_each = each.value == null ? [""] : [] + content { + default = true + } + } +} + +resource "google_folder_organization_policy" "list" { + for_each = var.policy_list + folder = local.folder.name + constraint = each.key + + dynamic "list_policy" { + for_each = each.value.status == null ? [] : [each.value] + iterator = policy + content { + inherit_from_parent = policy.value.inherit_from_parent + suggested_value = policy.value.suggested_value + dynamic "allow" { + for_each = policy.value.status ? [""] : [] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + dynamic "deny" { + for_each = policy.value.status ? [] : [""] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + } + } + + dynamic "restore_policy" { + for_each = each.value.status == null ? [true] : [] + content { + default = true + } + } +} diff --git a/examples/guardrails/cloudbuild/folder-factory/modules/folder/outputs.tf b/examples/guardrails/cloudbuild/folder-factory/modules/folder/outputs.tf new file mode 100644 index 0000000..37babc6 --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/modules/folder/outputs.tf @@ -0,0 +1,51 @@ +/** + * 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 + * + * 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. + */ +output "firewall_policies" { + description = "Map of firewall policy resources created in this folder." + value = { for k, v in google_compute_firewall_policy.policy : k => v } +} + +output "firewall_policy_id" { + description = "Map of firewall policy ids created in this folder." + value = { for k, v in google_compute_firewall_policy.policy : k => v.id } +} + +output "folder" { + description = "Folder resource." + value = local.folder +} + +output "id" { + description = "Folder id." + value = local.folder.name + depends_on = [ + google_folder_iam_binding.authoritative, + google_folder_organization_policy.boolean, + google_folder_organization_policy.list + ] +} + +output "name" { + description = "Folder name." + value = local.folder.display_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 + } +} diff --git a/examples/guardrails/cloudbuild/folder-factory/modules/folder/tags.tf b/examples/guardrails/cloudbuild/folder-factory/modules/folder/tags.tf new file mode 100644 index 0000000..2cd2f2f --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/modules/folder/tags.tf @@ -0,0 +1,21 @@ +/** + * 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 + * + * 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. + */ + +resource "google_tags_tag_binding" "binding" { + for_each = coalesce(var.tag_bindings, {}) + parent = "//cloudresourcemanager.googleapis.com/${local.folder.id}" + tag_value = each.value +} diff --git a/examples/guardrails/cloudbuild/folder-factory/modules/folder/variables.tf b/examples/guardrails/cloudbuild/folder-factory/modules/folder/variables.tf new file mode 100644 index 0000000..a3f32e3 --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/modules/folder/variables.tf @@ -0,0 +1,151 @@ +/** + * 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 + * + * 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. + */ + +variable "contacts" { + description = "List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES." + type = map(list(string)) + default = {} + nullable = false +} + +variable "firewall_policies" { + description = "Hierarchical firewall policies created in this folder." + type = map(map(object({ + action = string + description = string + direction = string + logging = bool + ports = map(list(string)) + priority = number + ranges = list(string) + target_resources = list(string) + target_service_accounts = list(string) + }))) + default = {} + nullable = false +} + +variable "firewall_policy_association" { + description = "The hierarchical firewall policy to associate to this folder. Must be either a key in the `firewall_policies` map or the id of a policy defined somewhere else." + type = map(string) + default = {} + nullable = false +} + +variable "firewall_policy_factory" { + description = "Configuration for the firewall policy factory." + type = object({ + cidr_file = string + policy_name = string + rules_file = string + }) + default = null +} + +variable "folder_create" { + description = "Create folder. When set to false, uses id to reference an existing folder." + type = bool + default = true +} + +variable "group_iam" { + description = "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." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam" { + description = "IAM bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "id" { + description = "Folder ID in case you use folder_create=false." + type = string + default = null +} + +variable "logging_exclusions" { + description = "Logging exclusions for this folder in the form {NAME -> FILTER}." + type = map(string) + default = {} + nullable = false +} + +variable "logging_sinks" { + description = "Logging sinks to create for this folder." + type = map(object({ + destination = string + type = string + filter = string + include_children = bool + # TODO exclusions also support description and disabled + exclusions = map(string) + })) + validation { + condition = alltrue([ + for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } + default = {} + nullable = false +} + +variable "name" { + description = "Folder name." + type = string + default = null +} + +variable "parent" { + description = "Parent in folders/folder_id or organizations/org_id format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "policy_boolean" { + description = "Map of boolean org policies and enforcement value, set value to null for policy restore." + type = map(bool) + default = {} + nullable = false +} + +variable "policy_list" { + description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." + type = map(object({ + inherit_from_parent = bool + suggested_value = string + status = bool + values = list(string) + })) + default = {} + nullable = false +} + +variable "tag_bindings" { + description = "Tag bindings for this folder, in key => tag value id format." + type = map(string) + default = null +} diff --git a/examples/guardrails/cloudbuild/folder-factory/modules/folder/versions.tf b/examples/guardrails/cloudbuild/folder-factory/modules/folder/versions.tf new file mode 100644 index 0000000..e72a780 --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/modules/folder/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.1.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/cloudbuild/folder-factory/outputs.tf b/examples/guardrails/cloudbuild/folder-factory/outputs.tf new file mode 100644 index 0000000..a84b9d5 --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/outputs.tf @@ -0,0 +1,20 @@ +/** + * 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 + * + * 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. + */ + +output "folders" { + description = "Created folders." + value = module.folder +} \ No newline at end of file diff --git a/examples/guardrails/cloudbuild/folder-factory/provider.tf b/examples/guardrails/cloudbuild/folder-factory/provider.tf new file mode 100644 index 0000000..0e17ef9 --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/provider.tf @@ -0,0 +1,28 @@ +/** + * 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 + * + * 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. + */ + +terraform { + backend "gcs" { + } +} + +provider "google" { + +} + +provider "google-beta" { + +} diff --git a/examples/guardrails/cloudbuild/folder-factory/variables.tf b/examples/guardrails/cloudbuild/folder-factory/variables.tf new file mode 100644 index 0000000..156a5ac --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/variables.tf @@ -0,0 +1,15 @@ +/** + * 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 + * + * 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. + */ \ No newline at end of file diff --git a/examples/guardrails/cloudbuild/project-factory/.cloudbuild/workflows/README.md b/examples/guardrails/cloudbuild/project-factory/.cloudbuild/workflows/README.md new file mode 100644 index 0000000..6473211 --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/.cloudbuild/workflows/README.md @@ -0,0 +1,43 @@ +# Build Repository from Gitlab + +Gitlab repository can be build with Cloud build by webhook triggers. This document covers the steps to setup webhooks triggers to build repository from gitlab: + +## Prerequisites + +* Enable the Cloud Build and secret manager API +* [Enable ssh access on gitlab](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#enabling_ssh_access_on_gitlab). + + +## Setup SSH Keys + +1. To access the gitlab code, ssh keys needs to be retrieved in the inline build config. +2. Follow the steps in [this](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#creating_an_ssh_key) document to create ssh keys. +3. [Add your public ssh access keys on gitlab](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#adding_your_public_ssh_access_key_on_gitlab). +4. [Add ssh key credentials in Secret Manager](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#webhook_triggers_create_store_secret). +5. [Create a webhook trigger from the GCP console](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#creating_webhook_triggers). +6. [Create a webhook in gitlab](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#creating_a_webhook_in_gitlab) using the webhook URL generated in step #5. + + +## Update variable substitutions in webhook trigger + + +Following substitutions variables needs to be configured in cloud build trigger settings to complete cloud build trigger setup. + +variables: +``` + _BRANCH: $(body.ref) + # This variable provides information about the branch which invoked the trigger. Substitute its value with $(body.ref) + _SECRET: 'projects//secrets//versions/' + # It contains the URI of a secret manager resource with an ssh key which has access to the target gitlab repository. E.g. projects/123456789/secrets/gitlab-ssh/versions/1 + _FOLDER: 'XXXX' + # It contains the directory with terraform configuration files which needs to be build when the trigger is invoked. E.g. examples/guardrails/skunkworks + _REPOSITORY_NAME: $(body.repository.name) + # It contains the name of the repository. Substitute its value with $(body.repository.name) + _STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + _TO_SHA: $(body.after) + # It contains the SHA of the commit that invoked the build. Substitute its value with $(body.after) + _SSH_REPOSITORY_NAME: 'XXXX' + # It contains the gitlab URL of the repository in SSH format. E.g. git@gitlab.com:gitlab-org/cloud-build-terraform-iac.git +``` + diff --git a/examples/guardrails/cloudbuild/project-factory/.cloudbuild/workflows/cloudbuild.yaml b/examples/guardrails/cloudbuild/project-factory/.cloudbuild/workflows/cloudbuild.yaml new file mode 100644 index 0000000..af97872 --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/.cloudbuild/workflows/cloudbuild.yaml @@ -0,0 +1,93 @@ +# 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 +# +# 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. + +steps: +# Setup SSH: +# 1- save the SSH key from Secret Manager to a file +# 2- add the host key to the known_hosts file + - name: gcr.io/cloud-builders/git + args: + - '-c' + - | + echo "$$SSHKEY" > /root/.ssh/id_rsa + chmod 400 /root/.ssh/id_rsa + ssh-keyscan gitlab.com > /root/.ssh/known_hosts + entrypoint: bash + secretEnv: + - SSHKEY + volumes: + - name: ssh + path: /root/.ssh + + # Clone the repository + - name: gcr.io/cloud-builders/git + args: + - clone + - '-n' + - $_SSH_REPOSITORY_NAME + - . + volumes: + - name: ssh + path: /root/.ssh + + # Checkout the specific commit that invoked this build + - name: gcr.io/cloud-builders/git + args: + - checkout + - $_TO_SHA + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - id: 'Terraform Init' + name: 'hashicorp/terraform' + dir: $_FOLDER + entrypoint: 'sh' + args: + - '-c' + - | + terraform init \ + -backend-config="bucket=$_STATE_BUCKET" \ + -backend-config="prefix=$_REPOSITORY_NAME" \ + + # Generates an execution plan for Terraform + - id: ' Terraform Plan' + name: 'hashicorp/terraform' + dir: $_FOLDER + entrypoint: 'sh' + args: + - '-c' + - | + terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + - id: 'Terraform Apply' + name: 'hashicorp/terraform' + dir: $_FOLDER + entrypoint: 'sh' + args: + - '-c' + - | + if [ "$_BRANCH" == "refs/heads/main" ];then + terraform apply -auto-approve + fi + +# This field is used for secret from Secret Manager with Cloud Build. +availableSecrets: + secretManager: + - versionName: $_SECRET + env: SSHKEY + +# Use this option to specify the logs location. With CLOUD_LOGGING_ONLY, logs are stored in Cloud Logging. See the document for more logging options https://cloud.google.com/build/docs/api/reference/rest/v1/projects.builds#loggingmode. +options: + logging: CLOUD_LOGGING_ONLY + \ No newline at end of file diff --git a/examples/guardrails/project-factory/.github/workflows/terraform-deployment.yml b/examples/guardrails/cloudbuild/project-factory/.github/workflows/terraform-deployment.yml similarity index 100% rename from examples/guardrails/project-factory/.github/workflows/terraform-deployment.yml rename to examples/guardrails/cloudbuild/project-factory/.github/workflows/terraform-deployment.yml diff --git a/examples/guardrails/cloudbuild/project-factory/.gitignore b/examples/guardrails/cloudbuild/project-factory/.gitignore new file mode 100644 index 0000000..a5d8c0e --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.terraform +.terraform.lock.hcl diff --git a/examples/guardrails/cloudbuild/project-factory/README.md b/examples/guardrails/cloudbuild/project-factory/README.md new file mode 100644 index 0000000..f901e7f --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/README.md @@ -0,0 +1,75 @@ +# Project Factory + +This is a template for a DevOps project factory. + +It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory) and is intended to house the projects of a specified folder: + +Overview + +Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. + +![Folder Factory](https://user-images.githubusercontent.com/94000358/169809882-f5ff9fb1-d037-49de-8c2c-bf0d457b662f.png) + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: + +Secret settings + +or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: +``` +env: + STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + FOLDER: 'folders/XXXX' + # The folder under which the projects should be created + WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' + # The workload identity provider that should be used for this repository. + SERVICE_ACCOUNT: 'XXXX@XXXX' + # The service account that should be used for this repository. +``` + +## Setting up projects + +The project factory will: +- create a service account with defined rights +- create a project within the folder +- connect the service account to the Github repository informantion + +It uses YAML configuration files for every project with the following sample structure: +``` +billing_account_id: XXXXXX-XXXXXX-XXXXXX +roles: + - roles/viewer + - roles/iam.serviceAccountUser + - roles/iam.securityReviewer + - roles/monitoring.viewer + - roles/monitoring.editor + - roles/monitoring.alertPolicyViewer + - roles/monitoring.alertPolicyEditor + - roles/monitoring.dashboardViewer + - roles/monitoring.dashboardEditor + - roles/monitoring.notificationChannelViewer + - roles/monitoring.notificationChannelEditor + - roles/monitoring.servicesViewer + - roles/monitoring.servicesEditor + - roles/monitoring.uptimeCheckConfigViewer + - roles/monitoring.uptimeCheckConfigEditor + - roles/secretmanager.viewer + - roles/secretmanager.secretVersionManager + - roles/secretmanager.admin + - roles/storage.admin + - roles/storage.objectAdmin + - roles/storage.objectCreator + - roles/storage.objectViewer +repo_provider: github +repo_name: devops-governance/skunkworks +repo_branch: dev +``` + +Every project is defined with its own file located in the [Project Folder](data/projects). diff --git a/examples/guardrails/cloudbuild/project-factory/data/projects/project.yaml.sample b/examples/guardrails/cloudbuild/project-factory/data/projects/project.yaml.sample new file mode 100644 index 0000000..d813f4b --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/data/projects/project.yaml.sample @@ -0,0 +1,41 @@ +# 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 +# +# 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. + +billing_account_id: XXXXXX-XXXXXX-XXXXXX +roles: + - roles/viewer + - roles/iam.serviceAccountUser + - roles/iam.securityReviewer + - roles/monitoring.viewer + - roles/monitoring.editor + - roles/monitoring.alertPolicyViewer + - roles/monitoring.alertPolicyEditor + - roles/monitoring.dashboardViewer + - roles/monitoring.dashboardEditor + - roles/monitoring.notificationChannelViewer + - roles/monitoring.notificationChannelEditor + - roles/monitoring.servicesViewer + - roles/monitoring.servicesEditor + - roles/monitoring.uptimeCheckConfigViewer + - roles/monitoring.uptimeCheckConfigEditor + - roles/secretmanager.viewer + - roles/secretmanager.secretVersionManager + - roles/secretmanager.admin + - roles/storage.admin + - roles/storage.objectAdmin + - roles/storage.objectCreator + - roles/storage.objectViewer +repo_provider: github +repo_name: github-org/github-repo +repo_branch: dev diff --git a/examples/guardrails/project-factory/main.tf b/examples/guardrails/cloudbuild/project-factory/main.tf similarity index 100% rename from examples/guardrails/project-factory/main.tf rename to examples/guardrails/cloudbuild/project-factory/main.tf diff --git a/examples/guardrails/cloudbuild/project-factory/modules/project/README.md b/examples/guardrails/cloudbuild/project-factory/modules/project/README.md new file mode 100644 index 0000000..e4f2139 --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/modules/project/README.md @@ -0,0 +1,308 @@ +# Project Module + +## Examples + +### Minimal example with IAM + +```hcl +locals { + gke_service_account = "my_gke_service_account" +} + +module "project" { + source = "./modules/project" + billing_account = "123456-123456-123456" + name = "project-example" + parent = "folders/1234567890" + prefix = "foo" + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + iam = { + "roles/container.hostServiceAgentUser" = [ + "serviceAccount:${local.gke_service_account}" + ] + } +} +# tftest modules=1 resources=4 +``` + +### Minimal example with IAM additive roles + +```hcl +module "project" { + source = "./modules/project" + name = "project-example" + + iam_additive = { + "roles/viewer" = [ + "group:one@example.org", "group:two@xample.org" + ], + "roles/storage.objectAdmin" = [ + "group:two@example.org" + ], + "roles/owner" = [ + "group:three@example.org" + ], + } +} +# tftest modules=1 resources=5 +``` + +### Shared VPC service + +```hcl +module "project" { + source = "./modules/project" + name = "project-example" + + shared_vpc_service_config = { + attach = true + host_project = "my-host-project" + service_identity_iam = { + "roles/compute.networkUser" = [ + "cloudservices", "container-engine" + ] + "roles/vpcaccess.user" = [ + "cloudrun" + ] + "roles/container.hostServiceAgentUser" = [ + "container-engine" + ] + } + } +} +# tftest modules=1 resources=6 +``` + +### Organization policies + +```hcl +module "project" { + source = "./modules/project" + billing_account = "123456-123456-123456" + name = "project-example" + parent = "folders/1234567890" + prefix = "foo" + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + policy_boolean = { + "constraints/compute.disableGuestAttributesAccess" = true + "constraints/compute.skipDefaultNetworkCreation" = true + } + policy_list = { + "constraints/compute.trustedImageProjects" = { + inherit_from_parent = null + suggested_value = null + status = true + values = ["projects/my-project"] + } + } +} +# tftest modules=1 resources=6 +``` + +## Logging Sinks + +```hcl +module "gcs" { + source = "./modules/gcs" + project_id = var.project_id + name = "gcs_sink" + force_destroy = true +} + +module "dataset" { + source = "./modules/bigquery-dataset" + project_id = var.project_id + id = "bq_sink" +} + +module "pubsub" { + source = "./modules/pubsub" + project_id = var.project_id + name = "pubsub_sink" +} + +module "bucket" { + source = "./modules/logging-bucket" + parent_type = "project" + parent = "my-project" + id = "bucket" +} + +module "project-host" { + source = "./modules/project" + name = "my-project" + billing_account = "123456-123456-123456" + parent = "folders/1234567890" + logging_sinks = { + warnings = { + type = "storage" + destination = module.gcs.name + filter = "severity=WARNING" + iam = false + unique_writer = false + exclusions = {} + } + info = { + type = "bigquery" + destination = module.dataset.id + filter = "severity=INFO" + iam = false + unique_writer = false + exclusions = {} + } + notice = { + type = "pubsub" + destination = module.pubsub.id + filter = "severity=NOTICE" + iam = true + unique_writer = false + exclusions = {} + } + debug = { + type = "logging" + destination = module.bucket.id + filter = "severity=DEBUG" + iam = true + unique_writer = false + exclusions = { + no-compute = "logName:compute" + } + } + } + logging_exclusions = { + no-gce-instances = "resource.type=gce_instance" + } +} +# tftest modules=5 resources=12 +``` + +## Cloud KMS encryption keys + +```hcl +module "project" { + source = "./modules/project" + name = "my-project" + billing_account = "123456-123456-123456" + prefix = "foo" + services = [ + "compute.googleapis.com", + "storage.googleapis.com" + ] + service_encryption_key_ids = { + compute = [ + "projects/kms-central-prj/locations/europe-west3/keyRings/my-keyring/cryptoKeys/europe3-gce", + "projects/kms-central-prj/locations/europe-west4/keyRings/my-keyring/cryptoKeys/europe4-gce" + ] + storage = [ + "projects/kms-central-prj/locations/europe/keyRings/my-keyring/cryptoKeys/europe-gcs" + ] + } +} +# tftest modules=1 resources=7 +``` + +## 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. + +```hcl +module "org" { + source = "./modules/organization" + organization_id = var.organization_id + tags = { + environment = { + description = "Environment specification." + iam = null + values = { + dev = null + prod = null + } + } + } +} + +module "project" { + source = "./modules/project" + name = "test-project" + tag_bindings = { + env-prod = module.org.tag_values["environment/prod"].id + foo = "tagValues/12345678" + } +} +# tftest modules=2 resources=6 +``` + + + + +## Files + +| name | description | resources | +|---|---|---| +| [iam.tf](./iam.tf) | Generic and OSLogin-specific IAM bindings and roles. | google_project_iam_binding · google_project_iam_custom_role · google_project_iam_member | +| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_project_exclusion · google_logging_project_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | +| [main.tf](./main.tf) | Module-level locals and resources. | google_compute_project_metadata_item · google_essential_contacts_contact · google_monitoring_monitored_project · google_project · google_project_service · google_resource_manager_lien | +| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_project_organization_policy | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [service-accounts.tf](./service-accounts.tf) | Service identities and supporting resources. | google_kms_crypto_key_iam_member · google_project_service_identity | +| [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project · google_compute_shared_vpc_service_project · google_project_iam_member | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding | +| [variables.tf](./variables.tf) | Module variables. | | +| [versions.tf](./versions.tf) | Version pins. | | +| [vpc-sc.tf](./vpc-sc.tf) | VPC-SC project-level perimeter configuration. | google_access_context_manager_service_perimeter_resource | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [name](variables.tf#L125) | Project name and id suffix. | string | ✓ | | +| [auto_create_network](variables.tf#L17) | Whether to create the default network for the project. | bool | | false | +| [billing_account](variables.tf#L23) | Billing account id. | string | | null | +| [contacts](variables.tf#L29) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | +| [custom_roles](variables.tf#L36) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| [descriptive_name](variables.tf#L43) | Name of the project name. Used for project name instead of `name` variable. | string | | null | +| [group_iam](variables.tf#L49) | 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. | map(list(string)) | | {} | +| [iam](variables.tf#L56) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive](variables.tf#L63) | IAM additive bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive_members](variables.tf#L70) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | map(list(string)) | | {} | +| [labels](variables.tf#L76) | Resource labels. | map(string) | | {} | +| [lien_reason](variables.tf#L83) | If non-empty, creates a project lien with this description. | string | | "" | +| [logging_exclusions](variables.tf#L89) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L96) | Logging sinks to create for this project. | map(object({…})) | | {} | +| [metric_scopes](variables.tf#L118) | List of projects that will act as metric scopes for this project. | list(string) | | [] | +| [oslogin](variables.tf#L130) | Enable OS Login. | bool | | false | +| [oslogin_admins](variables.tf#L136) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | +| [oslogin_users](variables.tf#L144) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | +| [parent](variables.tf#L151) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [policy_boolean](variables.tf#L161) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | +| [policy_list](variables.tf#L168) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | +| [prefix](variables.tf#L180) | Prefix used to generate project id and name. | string | | null | +| [project_create](variables.tf#L186) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | +| [service_config](variables.tf#L192) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L204) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | +| [service_perimeter_bridges](variables.tf#L211) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | +| [service_perimeter_standard](variables.tf#L218) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | +| [services](variables.tf#L224) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L230) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L239) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | +| [skip_delete](variables.tf#L249) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [tag_bindings](variables.tf#L255) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [custom_roles](outputs.tf#L17) | Ids of the created custom roles. | | +| [name](outputs.tf#L25) | Project name. | | +| [number](outputs.tf#L38) | Project number. | | +| [project_id](outputs.tf#L51) | Project id. | | +| [service_accounts](outputs.tf#L66) | Product robot service accounts in project. | | +| [sink_writer_identities](outputs.tf#L82) | Writer identities created for each sink. | | + + diff --git a/examples/guardrails/cloudbuild/project-factory/modules/project/iam.tf b/examples/guardrails/cloudbuild/project-factory/modules/project/iam.tf new file mode 100644 index 0000000..69925cc --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/modules/project/iam.tf @@ -0,0 +1,115 @@ +/** + * 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 + * + * 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 Generic and OSLogin-specific IAM bindings and roles. + +# IAM notes: +# - external users need to have accepted the invitation email to join +# - oslogin roles also require role to list instances +# - additive (non-authoritative) roles might fail due to dynamic values + +locals { + _group_iam_roles = distinct(flatten(values(var.group_iam))) + _group_iam = { + for r in local._group_iam_roles : r => [ + 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( + try(var.iam[role], []), + 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_project_iam_custom_role" "roles" { + for_each = var.custom_roles + project = local.project.project_id + role_id = each.key + title = "Custom role ${each.key}" + description = "Terraform-managed." + permissions = each.value +} + +resource "google_project_iam_binding" "authoritative" { + for_each = local.iam + project = local.project.project_id + role = each.key + members = each.value + depends_on = [ + google_project_service.project_services, + google_project_iam_custom_role.roles + ] +} + +resource "google_project_iam_member" "additive" { + for_each = ( + length(var.iam_additive) + length(var.iam_additive_members) > 0 + ? local.iam_additive + : {} + ) + project = local.project.project_id + role = each.value.role + member = each.value.member + depends_on = [ + google_project_service.project_services, + google_project_iam_custom_role.roles + ] +} + +resource "google_project_iam_member" "oslogin_iam_serviceaccountuser" { + for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([]) + project = local.project.project_id + role = "roles/iam.serviceAccountUser" + member = each.value +} + +resource "google_project_iam_member" "oslogin_compute_viewer" { + for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([]) + project = local.project.project_id + role = "roles/compute.viewer" + member = each.value +} + +resource "google_project_iam_member" "oslogin_admins" { + for_each = var.oslogin ? toset(var.oslogin_admins) : toset([]) + project = local.project.project_id + role = "roles/compute.osAdminLogin" + member = each.value +} + +resource "google_project_iam_member" "oslogin_users" { + for_each = var.oslogin ? toset(var.oslogin_users) : toset([]) + project = local.project.project_id + role = "roles/compute.osLogin" + member = each.value +} diff --git a/examples/guardrails/cloudbuild/project-factory/modules/project/logging.tf b/examples/guardrails/cloudbuild/project-factory/modules/project/logging.tf new file mode 100644 index 0000000..04d7abf --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/modules/project/logging.tf @@ -0,0 +1,91 @@ +/** + * 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 + * + * 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 Log sinks and supporting resources. + +locals { + sink_bindings = { + for type in ["bigquery", "pubsub", "logging", "storage"] : + type => { + for name, sink in var.logging_sinks : + name => sink if sink.iam && sink.type == type + } + } +} + +resource "google_logging_project_sink" "sink" { + for_each = var.logging_sinks + name = each.key + #description = "${each.key} (Terraform-managed)." + project = local.project.project_id + destination = "${each.value.type}.googleapis.com/${each.value.destination}" + filter = each.value.filter + unique_writer_identity = each.value.unique_writer + + dynamic "exclusions" { + for_each = each.value.exclusions + iterator = exclusion + content { + name = exclusion.key + filter = exclusion.value + } + } + + depends_on = [ + google_project_iam_binding.authoritative, + google_project_iam_member.additive + ] +} + +resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { + for_each = local.sink_bindings["storage"] + bucket = each.value.destination + role = "roles/storage.objectCreator" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" { + for_each = local.sink_bindings["bigquery"] + project = split("/", each.value.destination)[1] + dataset_id = split("/", each.value.destination)[3] + role = "roles/bigquery.dataEditor" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" { + for_each = local.sink_bindings["pubsub"] + project = split("/", each.value.destination)[1] + topic = split("/", each.value.destination)[3] + role = "roles/pubsub.publisher" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_project_iam_member" "bucket-sinks-binding" { + for_each = local.sink_bindings["logging"] + project = split("/", each.value.destination)[1] + role = "roles/logging.bucketWriter" + member = google_logging_project_sink.sink[each.key].writer_identity + # TODO(jccb): use a condition to limit writer-identity only to this + # bucket +} + +resource "google_logging_project_exclusion" "logging-exclusion" { + for_each = var.logging_exclusions + name = each.key + project = local.project.project_id + description = "${each.key} (Terraform-managed)." + filter = each.value +} diff --git a/examples/guardrails/cloudbuild/project-factory/modules/project/main.tf b/examples/guardrails/cloudbuild/project-factory/modules/project/main.tf new file mode 100644 index 0000000..9f0dff4 --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/modules/project/main.tf @@ -0,0 +1,95 @@ +/** + * 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 + * + * 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 { + descriptive_name = ( + var.descriptive_name != null ? var.descriptive_name : "${local.prefix}${var.name}" + ) + parent_type = var.parent == null ? null : split("/", var.parent)[0] + parent_id = var.parent == null ? null : split("/", var.parent)[1] + prefix = var.prefix == null ? "" : "${var.prefix}-" + project = ( + var.project_create ? + { + project_id = try(google_project.project.0.project_id, null) + number = try(google_project.project.0.number, null) + name = try(google_project.project.0.name, null) + } + : { + project_id = "${local.prefix}${var.name}" + number = try(data.google_project.project.0.number, null) + name = try(data.google_project.project.0.name, null) + } + ) +} + +data "google_project" "project" { + count = var.project_create ? 0 : 1 + project_id = "${local.prefix}${var.name}" +} + +resource "google_project" "project" { + count = var.project_create ? 1 : 0 + org_id = local.parent_type == "organizations" ? local.parent_id : null + folder_id = local.parent_type == "folders" ? local.parent_id : null + project_id = "${local.prefix}${var.name}" + name = local.descriptive_name + billing_account = var.billing_account + auto_create_network = var.auto_create_network + labels = var.labels + skip_delete = var.skip_delete +} + +resource "google_project_service" "project_services" { + for_each = toset(var.services) + project = local.project.project_id + service = each.value + disable_on_destroy = var.service_config.disable_on_destroy + disable_dependent_services = var.service_config.disable_dependent_services +} + +resource "google_compute_project_metadata_item" "oslogin_meta" { + count = var.oslogin ? 1 : 0 + project = local.project.project_id + key = "enable-oslogin" + value = "TRUE" + # depend on services or it will fail on destroy + depends_on = [google_project_service.project_services] +} + +resource "google_resource_manager_lien" "lien" { + count = var.lien_reason != "" ? 1 : 0 + parent = "projects/${local.project.number}" + restrictions = ["resourcemanager.projects.delete"] + origin = "created-by-terraform" + reason = var.lien_reason +} + +resource "google_essential_contacts_contact" "contact" { + provider = google-beta + for_each = var.contacts + parent = "projects/${local.project.project_id}" + email = each.key + language_tag = "en" + notification_category_subscriptions = each.value +} + +resource "google_monitoring_monitored_project" "primary" { + provider = google-beta + for_each = toset(var.metric_scopes) + metrics_scope = each.value + name = local.project.project_id +} diff --git a/examples/guardrails/cloudbuild/project-factory/modules/project/organization-policies.tf b/examples/guardrails/cloudbuild/project-factory/modules/project/organization-policies.tf new file mode 100644 index 0000000..6870754 --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/modules/project/organization-policies.tf @@ -0,0 +1,90 @@ +/** + * 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 + * + * 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 Project-level organization policies. + +resource "google_project_organization_policy" "boolean" { + for_each = var.policy_boolean + project = local.project.project_id + constraint = each.key + + dynamic "boolean_policy" { + for_each = each.value == null ? [] : [each.value] + iterator = policy + content { + enforced = policy.value + } + } + + dynamic "restore_policy" { + for_each = each.value == null ? [""] : [] + content { + default = true + } + } +} + +resource "google_project_organization_policy" "list" { + for_each = var.policy_list + project = local.project.project_id + constraint = each.key + + dynamic "list_policy" { + for_each = each.value.status == null ? [] : [each.value] + iterator = policy + content { + inherit_from_parent = policy.value.inherit_from_parent + suggested_value = policy.value.suggested_value + dynamic "allow" { + for_each = policy.value.status ? [""] : [] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + dynamic "deny" { + for_each = policy.value.status ? [] : [""] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + } + } + + dynamic "restore_policy" { + for_each = each.value.status == null ? [true] : [] + content { + default = true + } + } +} diff --git a/examples/guardrails/cloudbuild/project-factory/modules/project/outputs.tf b/examples/guardrails/cloudbuild/project-factory/modules/project/outputs.tf new file mode 100644 index 0000000..10d0e55 --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/modules/project/outputs.tf @@ -0,0 +1,87 @@ +/** + * 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 + * + * 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. + */ + +output "custom_roles" { + description = "Ids of the created custom roles." + value = { + for name, role in google_project_iam_custom_role.roles : + name => role.id + } +} + +output "name" { + description = "Project name." + value = local.project.name + depends_on = [ + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "number" { + description = "Project number." + value = local.project.number + depends_on = [ + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "project_id" { + description = "Project id." + value = "${local.prefix}${var.name}" + depends_on = [ + google_project.project, + data.google_project.project, + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "service_accounts" { + description = "Product robot service accounts in project." + value = { + cloud_services = local.service_account_cloud_services + default = local.service_accounts_default + robots = local.service_accounts_robots + } + depends_on = [ + google_project_service.project_services, + google_kms_crypto_key_iam_member.service_identity_cmek, + google_project_service_identity.jit_si, + data.google_bigquery_default_service_account.bq_sa, + data.google_storage_project_service_account.gcs_sa + ] +} + +output "sink_writer_identities" { + description = "Writer identities created for each sink." + value = { + for name, sink in google_logging_project_sink.sink : name => sink.writer_identity + } +} diff --git a/examples/guardrails/cloudbuild/project-factory/modules/project/service-accounts.tf b/examples/guardrails/cloudbuild/project-factory/modules/project/service-accounts.tf new file mode 100644 index 0000000..3423524 --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/modules/project/service-accounts.tf @@ -0,0 +1,113 @@ +/** + * 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 + * + * 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 Service identities and supporting resources. + +locals { + _service_accounts_cmek_service_dependencies = { + "composer" : [ + "composer", + "artifactregistry", "container-engine", "compute", "pubsub", "storage" + ] + "dataflow" : ["dataflow", "compute"] + } + _service_accounts_robot_services = { + artifactregistry = "service-%s@gcp-sa-artifactregistry" + bq = "bq-%s@bigquery-encryption" + cloudasset = "service-%s@gcp-sa-cloudasset" + cloudbuild = "service-%s@gcp-sa-cloudbuild" + cloudfunctions = "service-%s@gcf-admin-robot" + cloudrun = "service-%s@serverless-robot-prod" + composer = "service-%s@cloudcomposer-accounts" + compute = "service-%s@compute-system" + container-engine = "service-%s@container-engine-robot" + containerregistry = "service-%s@containerregistry" + dataflow = "service-%s@dataflow-service-producer-prod" + dataproc = "service-%s@dataproc-accounts" + gae-flex = "service-%s@gae-api-prod" + # TODO: deprecate gcf + gcf = "service-%s@gcf-admin-robot" + pubsub = "service-%s@gcp-sa-pubsub" + secretmanager = "service-%s@gcp-sa-secretmanager" + storage = "service-%s@gs-project-accounts" + } + service_accounts_default = { + compute = "${local.project.number}-compute@developer.gserviceaccount.com" + gae = "${local.project.project_id}@appspot.gserviceaccount.com" + } + service_account_cloud_services = ( + "${local.project.number}@cloudservices.gserviceaccount.com" + ) + service_accounts_robots = { + for k, v in local._service_accounts_robot_services : + k => "${format(v, local.project.number)}.iam.gserviceaccount.com" + } + service_accounts_jit_services = [ + "secretmanager.googleapis.com", + "pubsub.googleapis.com", + "cloudasset.googleapis.com" + ] + service_accounts_cmek_service_keys = distinct(flatten([ + for s in keys(var.service_encryption_key_ids) : [ + for ss in try(local._service_accounts_cmek_service_dependencies[s], [s]) : [ + for key in var.service_encryption_key_ids[s] : { + service = ss + key = key + } if key != null + ] + ] + ])) +} + +data "google_storage_project_service_account" "gcs_sa" { + count = contains(var.services, "storage.googleapis.com") ? 1 : 0 + project = local.project.project_id + depends_on = [google_project_service.project_services] +} + +data "google_bigquery_default_service_account" "bq_sa" { + count = contains(var.services, "bigquery.googleapis.com") ? 1 : 0 + project = local.project.project_id + depends_on = [google_project_service.project_services] +} + +# Secret Manager SA created just in time, we need to trigger the creation. +resource "google_project_service_identity" "jit_si" { + for_each = setintersection(var.services, local.service_accounts_jit_services) + provider = google-beta + project = local.project.project_id + service = each.value + depends_on = [google_project_service.project_services] +} + +resource "google_kms_crypto_key_iam_member" "service_identity_cmek" { + for_each = { + for service_key in local.service_accounts_cmek_service_keys : + "${service_key.service}.${service_key.key}" => service_key + if service_key != service_key.key + } + crypto_key_id = each.value.key + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + member = "serviceAccount:${local.service_accounts_robots[each.value.service]}" + depends_on = [ + google_project.project, + google_project_service.project_services, + google_project_service_identity.jit_si, + data.google_bigquery_default_service_account.bq_sa, + data.google_project.project, + data.google_storage_project_service_account.gcs_sa, + ] +} diff --git a/examples/guardrails/cloudbuild/project-factory/modules/project/shared-vpc.tf b/examples/guardrails/cloudbuild/project-factory/modules/project/shared-vpc.tf new file mode 100644 index 0000000..9c7bd71 --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/modules/project/shared-vpc.tf @@ -0,0 +1,75 @@ +/** + * 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 + * + * 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 Shared VPC project-level configuration. + +locals { + # compute the host project IAM bindings for this project's service identities + _svpc_service_identity_iam = coalesce( + local.svpc_service_config.service_identity_iam, {} + ) + _svpc_service_iam = flatten([ + for role, services in local._svpc_service_identity_iam : [ + for service in services : { role = role, service = service } + ] + ]) + svpc_host_config = { + enabled = coalesce( + try(var.shared_vpc_host_config.enabled, null), false + ) + service_projects = coalesce( + try(var.shared_vpc_host_config.service_projects, null), [] + ) + } + svpc_service_config = coalesce(var.shared_vpc_service_config, { + host_project = null, service_identity_iam = {} + }) + svpc_service_iam = { + for b in local._svpc_service_iam : "${b.role}:${b.service}" => b + } +} + +resource "google_compute_shared_vpc_host_project" "shared_vpc_host" { + provider = google-beta + count = local.svpc_host_config.enabled ? 1 : 0 + project = local.project.project_id +} + +resource "google_compute_shared_vpc_service_project" "service_projects" { + provider = google-beta + for_each = toset(local.svpc_host_config.service_projects) + host_project = local.project.project_id + service_project = each.value + depends_on = [google_compute_shared_vpc_host_project.shared_vpc_host] +} + +resource "google_compute_shared_vpc_service_project" "shared_vpc_service" { + provider = google-beta + count = local.svpc_service_config.host_project != null ? 1 : 0 + host_project = var.shared_vpc_service_config.host_project + service_project = local.project.project_id +} + +resource "google_project_iam_member" "shared_vpc_host_robots" { + for_each = local.svpc_service_iam + project = var.shared_vpc_service_config.host_project + role = each.value.role + member = ( + each.value.service == "cloudservices" + ? "serviceAccount:${local.service_account_cloud_services}" + : "serviceAccount:${local.service_accounts_robots[each.value.service]}" + ) +} diff --git a/examples/guardrails/cloudbuild/project-factory/modules/project/tags.tf b/examples/guardrails/cloudbuild/project-factory/modules/project/tags.tf new file mode 100644 index 0000000..683143b --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/modules/project/tags.tf @@ -0,0 +1,21 @@ +/** + * 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 + * + * 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. + */ + +resource "google_tags_tag_binding" "binding" { + for_each = coalesce(var.tag_bindings, {}) + parent = "//cloudresourcemanager.googleapis.com/projects/${local.project.number}" + tag_value = each.value +} diff --git a/examples/guardrails/cloudbuild/project-factory/modules/project/variables.tf b/examples/guardrails/cloudbuild/project-factory/modules/project/variables.tf new file mode 100644 index 0000000..578f9d2 --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/modules/project/variables.tf @@ -0,0 +1,259 @@ +/** + * 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 + * + * 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. + */ + +variable "auto_create_network" { + description = "Whether to create the default network for the project." + type = bool + default = false +} + +variable "billing_account" { + description = "Billing account id." + type = string + default = null +} + +variable "contacts" { + description = "List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES." + type = map(list(string)) + default = {} + nullable = false +} + +variable "custom_roles" { + description = "Map of role name => list of permissions to create in this project." + type = map(list(string)) + default = {} + nullable = false +} + +variable "descriptive_name" { + description = "Name of the project name. Used for project name instead of `name` variable." + type = string + default = null +} + +variable "group_iam" { + description = "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." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam" { + description = "IAM bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_additive" { + description = "IAM additive bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +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 "labels" { + description = "Resource labels." + type = map(string) + default = {} + nullable = false +} + +variable "lien_reason" { + description = "If non-empty, creates a project lien with this description." + type = string + default = "" +} + +variable "logging_exclusions" { + description = "Logging exclusions for this project in the form {NAME -> FILTER}." + type = map(string) + default = {} + nullable = false +} + +variable "logging_sinks" { + description = "Logging sinks to create for this project." + type = map(object({ + destination = string + type = string + filter = string + iam = bool + unique_writer = bool + # TODO exclusions also support description and disabled + exclusions = map(string) + })) + validation { + condition = alltrue([ + for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } + default = {} + nullable = false +} + +variable "metric_scopes" { + description = "List of projects that will act as metric scopes for this project." + type = list(string) + default = [] + nullable = false +} + +variable "name" { + description = "Project name and id suffix." + type = string +} + +variable "oslogin" { + description = "Enable OS Login." + type = bool + default = false +} + +variable "oslogin_admins" { + description = "List of IAM-style identities that will be granted roles necessary for OS Login administrators." + type = list(string) + default = [] + nullable = false + +} + +variable "oslogin_users" { + description = "List of IAM-style identities that will be granted roles necessary for OS Login users." + type = list(string) + default = [] + nullable = false +} + +variable "parent" { + description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "policy_boolean" { + description = "Map of boolean org policies and enforcement value, set value to null for policy restore." + type = map(bool) + default = {} + nullable = false +} + +variable "policy_list" { + description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." + type = map(object({ + inherit_from_parent = bool + suggested_value = string + status = bool + values = list(string) + })) + default = {} + nullable = false +} + +variable "prefix" { + description = "Prefix used to generate project id and name." + type = string + default = null +} + +variable "project_create" { + description = "Create project. When set to false, uses a data source to reference existing project." + type = bool + default = true +} + +variable "service_config" { + description = "Configure service API activation." + type = object({ + disable_on_destroy = bool + disable_dependent_services = bool + }) + default = { + disable_on_destroy = true + disable_dependent_services = true + } +} + +variable "service_encryption_key_ids" { + description = "Cloud KMS encryption key in {SERVICE => [KEY_URL]} format." + type = map(list(string)) + default = {} +} + +# accessPolicies/ACCESS_POLICY_NAME/servicePerimeters/PERIMETER_NAME +variable "service_perimeter_bridges" { + description = "Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format." + type = list(string) + default = null +} + +# accessPolicies/ACCESS_POLICY_NAME/servicePerimeters/PERIMETER_NAME +variable "service_perimeter_standard" { + description = "Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format." + type = string + default = null +} + +variable "services" { + description = "Service APIs to enable." + type = list(string) + default = [] +} + +variable "shared_vpc_host_config" { + description = "Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project)." + type = object({ + enabled = bool + service_projects = list(string) + }) + default = null +} + +variable "shared_vpc_service_config" { + description = "Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config)." + # the list of valid service identities is in service-accounts.tf + type = object({ + host_project = string + service_identity_iam = map(list(string)) + }) + default = null +} + +variable "skip_delete" { + description = "Allows the underlying resources to be destroyed without destroying the project itself." + type = bool + default = false +} + +variable "tag_bindings" { + description = "Tag bindings for this project, in key => tag value id format." + type = map(string) + default = null +} diff --git a/examples/guardrails/cloudbuild/project-factory/modules/project/versions.tf b/examples/guardrails/cloudbuild/project-factory/modules/project/versions.tf new file mode 100644 index 0000000..e72a780 --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/modules/project/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.1.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/cloudbuild/project-factory/modules/project/vpc-sc.tf b/examples/guardrails/cloudbuild/project-factory/modules/project/vpc-sc.tf new file mode 100644 index 0000000..edaa203 --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/modules/project/vpc-sc.tf @@ -0,0 +1,45 @@ +/** + * 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 + * + * 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 VPC-SC project-level perimeter configuration. + +moved { + from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-standard + to = google_access_context_manager_service_perimeter_resource.standard +} + +resource "google_access_context_manager_service_perimeter_resource" "standard" { + count = var.service_perimeter_standard != null ? 1 : 0 + # this needs an additional lifecycle block in the vpc module on the + # google_access_context_manager_service_perimeter resource + perimeter_name = var.service_perimeter_standard + resource = "projects/${local.project.number}" +} + +moved { + from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-bridges + to = google_access_context_manager_service_perimeter_resource.bridge +} + +resource "google_access_context_manager_service_perimeter_resource" "bridge" { + for_each = toset( + var.service_perimeter_bridges != null ? var.service_perimeter_bridges : [] + ) + # this needs an additional lifecycle block in the vpc module on the + # google_access_context_manager_service_perimeter resource + perimeter_name = each.value + resource = "projects/${local.project.number}" +} diff --git a/examples/guardrails/cloudbuild/project-factory/modules/project_plus/README.md b/examples/guardrails/cloudbuild/project-factory/modules/project_plus/README.md new file mode 100644 index 0000000..2976a30 --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/modules/project_plus/README.md @@ -0,0 +1 @@ +This is an addon for the project module with connects one service account to one project. \ No newline at end of file diff --git a/examples/guardrails/project-factory/modules/project_plus/main.tf b/examples/guardrails/cloudbuild/project-factory/modules/project_plus/main.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project_plus/main.tf rename to examples/guardrails/cloudbuild/project-factory/modules/project_plus/main.tf diff --git a/examples/guardrails/cloudbuild/project-factory/modules/project_plus/outputs.tf b/examples/guardrails/cloudbuild/project-factory/modules/project_plus/outputs.tf new file mode 100644 index 0000000..c589d87 --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/modules/project_plus/outputs.tf @@ -0,0 +1,36 @@ +/** + * 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 + * + * 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. + */ + +output "project_id" { + description = "Project ID" + value = module.project.project_id +} + +output "service_account_email" { + description = "Service Account Email" + value = google_service_account.sa.email +} + +output "repo_sub" { + description = "Repository" + value = var.repo_sub +} + +output "repo_provider" { + description = "Repository Provider" + value = var.repo_provider +} + diff --git a/examples/guardrails/cloudbuild/project-factory/modules/project_plus/variables.tf b/examples/guardrails/cloudbuild/project-factory/modules/project_plus/variables.tf new file mode 100644 index 0000000..67524ae --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/modules/project_plus/variables.tf @@ -0,0 +1,52 @@ +/** + * 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 + * + * 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. + */ + + +variable "team" { + description = "Team name." + type = string +} + +variable "repo_sub" { + description = "Repository path" + type = string +} + +variable "repo_provider" { + description = "Repository provider" + type = string +} + +variable "billing_account" { + description = "Billing account name." + type = string +} + +variable "folder" { + description = "Folder name." + type = string +} + +variable "roles" { + description = "Roles to attach." + type = list(string) + default = [] +} + +variable "wif-pool" { + description = "WIF pool name." + type = string +} diff --git a/examples/guardrails/cloudbuild/project-factory/modules/project_plus/versions.tf b/examples/guardrails/cloudbuild/project-factory/modules/project_plus/versions.tf new file mode 100644 index 0000000..2904126 --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/modules/project_plus/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.0.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/project-factory/outputs.tf b/examples/guardrails/cloudbuild/project-factory/outputs.tf similarity index 100% rename from examples/guardrails/project-factory/outputs.tf rename to examples/guardrails/cloudbuild/project-factory/outputs.tf diff --git a/examples/guardrails/cloudbuild/project-factory/provider.tf b/examples/guardrails/cloudbuild/project-factory/provider.tf new file mode 100644 index 0000000..34beb2f --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/provider.tf @@ -0,0 +1,28 @@ +/** + * 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 + * + * 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. + */ + +terraform { + backend "gcs" { + } +} + +provider "google" { + +} + +provider "google-beta" { + +} diff --git a/examples/guardrails/project-factory/variables.tf b/examples/guardrails/cloudbuild/project-factory/variables.tf similarity index 100% rename from examples/guardrails/project-factory/variables.tf rename to examples/guardrails/cloudbuild/project-factory/variables.tf diff --git a/examples/guardrails/project-factory/wif.tf b/examples/guardrails/cloudbuild/project-factory/wif.tf similarity index 100% rename from examples/guardrails/project-factory/wif.tf rename to examples/guardrails/cloudbuild/project-factory/wif.tf diff --git a/examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/README.md b/examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/README.md new file mode 100644 index 0000000..b8b545e --- /dev/null +++ b/examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/README.md @@ -0,0 +1,47 @@ +# Build Repository from Gitlab + +Gitlab repository can be build with Cloud build by webhook triggers. This document covers the steps to setup webhooks triggers to build repository from gitlab: + +## Prerequisites + +* Enable the Cloud Build and secret manager API +* [Enable ssh access on gitlab](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#enabling_ssh_access_on_gitlab). + + +## Setup SSH Keys + +1. To access the gitlab code, ssh keys needs to be retrieved in the inline build config. +2. Follow the steps in [this](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#creating_an_ssh_key) document to create ssh keys. +3. [Add your public ssh access keys on gitlab](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#adding_your_public_ssh_access_key_on_gitlab). +4. [Add ssh key credentials in Secret Manager](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#webhook_triggers_create_store_secret). +5. [Create a webhook trigger from the GCP console](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#creating_webhook_triggers). +6. [Create a webhook in gitlab](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#creating_a_webhook_in_gitlab) using the webhook URL generated in step #5. + + +## Update variable substitutions in webhook trigger + + +Following substitutions variables needs to be configured in cloud build trigger settings to complete cloud build trigger setup. + +variables: +``` + _BRANCH: $(body.ref) + # This variable provides information about the branch which invoked the trigger. Substitute its value with $(body.ref) + _DEV_SECRET: 'projects//secrets//versions/' + # It contains the URI of a secret manager resource with an ssh key which has access to the target gitlab repository. E.g. projects/123456789/secrets/gitlab-ssh/versions/1. This variable is only required to be configured for configuring Skunkworks Development environment using tf-cloudbuild-dev.yaml file. + _STAGE_SECRET: 'projects//secrets//versions/' + # It contains the URI of a secret manager resource with an ssh key which has access to the target gitlab repository. E.g. projects/123456789/secrets/gitlab-ssh/versions/1. This variable is only required to be configured for configuring Skunkworks Staging environment using tf-cloudbuild-stage.yaml file. + _PROD_SECRET: 'projects//secrets//versions/' + # It contains the URI of a secret manager resource with an ssh key which has access to the target gitlab repository. E.g. projects/123456789/secrets/gitlab-ssh/versions/1. This variable is only required to be configured for configuring Skunkworks Production environment using tf-cloudbuild-prod.yaml file. + _FOLDER: 'XXXX' + # It contains the directory with terraform configuration files which needs to be build when the trigger is invoked. E.g. examples/guardrails/skunkworks + _REPOSITORY_NAME: $(body.repository.name) + # It contains the name of the repository. Substitute its value with $(body.repository.name) + _STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + _TO_SHA: $(body.after) + # It contains the SHA of the commit that invoked the build. Substitute its value with $(body.after) + _SSH_REPOSITORY_NAME: 'XXXX' + # It contains the gitlab URL of the repository in SSH format. E.g. git@gitlab.com:gitlab-org/cloud-build-terraform-iac.git +``` + diff --git a/examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/tf-cloudbuild-dev.yaml b/examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/tf-cloudbuild-dev.yaml new file mode 100644 index 0000000..ea28ac8 --- /dev/null +++ b/examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/tf-cloudbuild-dev.yaml @@ -0,0 +1,93 @@ +# 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 +# +# 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. + +steps: +# Setup SSH: +# 1- save the SSH key from Secret Manager to a file +# 2- add the host key to the known_hosts file + - name: gcr.io/cloud-builders/git + args: + - '-c' + - | + echo "$$SSHKEY" > /root/.ssh/id_rsa + chmod 400 /root/.ssh/id_rsa + ssh-keyscan gitlab.com > /root/.ssh/known_hosts + entrypoint: bash + secretEnv: + - SSHKEY + volumes: + - name: ssh + path: /root/.ssh + + # Clone the repository + - name: gcr.io/cloud-builders/git + args: + - clone + - '-n' + - $_SSH_REPOSITORY_NAME + - . + volumes: + - name: ssh + path: /root/.ssh + + # Checkout the specific commit that invoked this build + - name: gcr.io/cloud-builders/git + args: + - checkout + - $_TO_SHA + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - id: 'Terraform Init' + name: 'hashicorp/terraform' + dir: $_FOLDER + entrypoint: 'sh' + args: + - '-c' + - | + terraform init \ + -backend-config="bucket=$_STATE_BUCKET" \ + -backend-config="prefix=$_REPOSITORY_NAME" \ + + # Generates an execution plan for Terraform + - id: ' Terraform Plan' + name: 'hashicorp/terraform' + dir: $_FOLDER + entrypoint: 'sh' + args: + - '-c' + - | + terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + - id: 'Terraform Apply' + name: 'hashicorp/terraform' + dir: $_FOLDER + entrypoint: 'sh' + args: + - '-c' + - | + if [ "$_BRANCH" == "refs/heads/dev" ];then + terraform apply -auto-approve + fi + +# This field is used for secret from Secret Manager with Cloud Build. +availableSecrets: + secretManager: + - versionName: $_DEV_SECRET + env: SSHKEY + +# Use this option to specify the logs location. With CLOUD_LOGGING_ONLY, logs are stored in Cloud Logging. See the document for more logging options https://cloud.google.com/build/docs/api/reference/rest/v1/projects.builds#loggingmode. +options: + logging: CLOUD_LOGGING_ONLY + \ No newline at end of file diff --git a/examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/tf-cloudbuild-prod.yaml b/examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/tf-cloudbuild-prod.yaml new file mode 100644 index 0000000..d2de5e8 --- /dev/null +++ b/examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/tf-cloudbuild-prod.yaml @@ -0,0 +1,93 @@ +# 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 +# +# 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. + +steps: +# Setup SSH: +# 1- save the SSH key from Secret Manager to a file +# 2- add the host key to the known_hosts file + - name: gcr.io/cloud-builders/git + args: + - '-c' + - | + echo "$$SSHKEY" > /root/.ssh/id_rsa + chmod 400 /root/.ssh/id_rsa + ssh-keyscan gitlab.com > /root/.ssh/known_hosts + entrypoint: bash + secretEnv: + - SSHKEY + volumes: + - name: ssh + path: /root/.ssh + + # Clone the repository + - name: gcr.io/cloud-builders/git + args: + - clone + - '-n' + - $_SSH_REPOSITORY_NAME + - . + volumes: + - name: ssh + path: /root/.ssh + + # Checkout the specific commit that invoked this build + - name: gcr.io/cloud-builders/git + args: + - checkout + - $_TO_SHA + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - id: 'Terraform Init' + name: 'hashicorp/terraform' + dir: $_FOLDER + entrypoint: 'sh' + args: + - '-c' + - | + terraform init \ + -backend-config="bucket=$_STATE_BUCKET" \ + -backend-config="prefix=$_REPOSITORY_NAME" \ + + # Generates an execution plan for Terraform + - id: ' Terraform Plan' + name: 'hashicorp/terraform' + dir: $_FOLDER + entrypoint: 'sh' + args: + - '-c' + - | + terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + - id: 'Terraform Apply' + name: 'hashicorp/terraform' + dir: $_FOLDER + entrypoint: 'sh' + args: + - '-c' + - | + if [ "$_BRANCH" == "refs/heads/main" ];then + terraform apply -auto-approve + fi + +# This field is used for secret from Secret Manager with Cloud Build. +availableSecrets: + secretManager: + - versionName: $_PROD_SECRET + env: SSHKEY + +# Use this option to specify the logs location. With CLOUD_LOGGING_ONLY, logs are stored in Cloud Logging. See the document for more logging options https://cloud.google.com/build/docs/api/reference/rest/v1/projects.builds#loggingmode. +options: + logging: CLOUD_LOGGING_ONLY + \ No newline at end of file diff --git a/examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/tf-cloudbuild-stage.yaml b/examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/tf-cloudbuild-stage.yaml new file mode 100644 index 0000000..8549293 --- /dev/null +++ b/examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/tf-cloudbuild-stage.yaml @@ -0,0 +1,93 @@ +# 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 +# +# 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. + +steps: +# Setup SSH: +# 1- save the SSH key from Secret Manager to a file +# 2- add the host key to the known_hosts file + - name: gcr.io/cloud-builders/git + args: + - '-c' + - | + echo "$$SSHKEY" > /root/.ssh/id_rsa + chmod 400 /root/.ssh/id_rsa + ssh-keyscan gitlab.com > /root/.ssh/known_hosts + entrypoint: bash + secretEnv: + - SSHKEY + volumes: + - name: ssh + path: /root/.ssh + + # Clone the repository + - name: gcr.io/cloud-builders/git + args: + - clone + - '-n' + - $_SSH_REPOSITORY_NAME + - . + volumes: + - name: ssh + path: /root/.ssh + + # Checkout the specific commit that invoked this build + - name: gcr.io/cloud-builders/git + args: + - checkout + - $_TO_SHA + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - id: 'Terraform Init' + name: 'hashicorp/terraform' + dir: $_FOLDER + entrypoint: 'sh' + args: + - '-c' + - | + terraform init \ + -backend-config="bucket=$_STATE_BUCKET" \ + -backend-config="prefix=$_REPOSITORY_NAME" \ + + # Generates an execution plan for Terraform + - id: ' Terraform Plan' + name: 'hashicorp/terraform' + dir: $_FOLDER + entrypoint: 'sh' + args: + - '-c' + - | + terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + - id: 'Terraform Apply' + name: 'hashicorp/terraform' + dir: $_FOLDER + entrypoint: 'sh' + args: + - '-c' + - | + if [ "$_BRANCH" == "refs/heads/staging" ];then + terraform apply -auto-approve + fi + +# This field is used for secret from Secret Manager with Cloud Build. +availableSecrets: + secretManager: + - versionName: $_STAGE_SECRET + env: SSHKEY + +# Use this option to specify the logs location. With CLOUD_LOGGING_ONLY, logs are stored in Cloud Logging. See the document for more logging options https://cloud.google.com/build/docs/api/reference/rest/v1/projects.builds#loggingmode. +options: + logging: CLOUD_LOGGING_ONLY + \ No newline at end of file diff --git a/examples/guardrails/skunkworks/.github/workflows/tf-actions-dev.yml b/examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-dev.yml similarity index 100% rename from examples/guardrails/skunkworks/.github/workflows/tf-actions-dev.yml rename to examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-dev.yml diff --git a/examples/guardrails/skunkworks/.github/workflows/tf-actions-prod.yml b/examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-prod.yml similarity index 100% rename from examples/guardrails/skunkworks/.github/workflows/tf-actions-prod.yml rename to examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-prod.yml diff --git a/examples/guardrails/skunkworks/.github/workflows/tf-actions-stage.yml b/examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-stage.yml similarity index 100% rename from examples/guardrails/skunkworks/.github/workflows/tf-actions-stage.yml rename to examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-stage.yml diff --git a/examples/guardrails/cloudbuild/skunkworks/README.md b/examples/guardrails/cloudbuild/skunkworks/README.md new file mode 100644 index 0000000..0b89bb2 --- /dev/null +++ b/examples/guardrails/cloudbuild/skunkworks/README.md @@ -0,0 +1,29 @@ +# Skunkworks - IaC Kickstarter Template + +This is a template for an IaC kickstarter repository. + +![Skunkworks](https://user-images.githubusercontent.com/94000358/169810982-36f01de2-e5e5-4ecd-b98e-3cf5a6aa9f81.png) + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +This template creates a bucket in the specified target environment. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: + +Secret configuration + +or by modifing the [Workflow Action Files](.github/workflows/) and setting the environment variables: +``` +env: + STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' + # The workload identity provider that should be used for this repository. + SERVICE_ACCOUNT: 'XXXX@XXXX' + # The service account that should be used for this repository. +``` diff --git a/examples/guardrails/skunkworks/main.tf b/examples/guardrails/cloudbuild/skunkworks/main.tf similarity index 100% rename from examples/guardrails/skunkworks/main.tf rename to examples/guardrails/cloudbuild/skunkworks/main.tf diff --git a/examples/guardrails/cloudbuild/skunkworks/provider.tf b/examples/guardrails/cloudbuild/skunkworks/provider.tf new file mode 100644 index 0000000..0802a09 --- /dev/null +++ b/examples/guardrails/cloudbuild/skunkworks/provider.tf @@ -0,0 +1,28 @@ +/** + * 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 + * + * 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. + */ + +terraform { + backend "gcs" { + } +} + +provider "google" { + +} + +provider "google-beta" { + +} \ No newline at end of file diff --git a/examples/guardrails/skunkworks/variables.tf b/examples/guardrails/cloudbuild/skunkworks/variables.tf similarity index 100% rename from examples/guardrails/skunkworks/variables.tf rename to examples/guardrails/cloudbuild/skunkworks/variables.tf diff --git a/examples/guardrails/folder-factory/.terraform-enterprise/worflows/cloudbuild.yaml b/examples/guardrails/folder-factory/.terraform-enterprise/worflows/cloudbuild.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/examples/guardrails/github/folder-factory/.github/workflows/terraform-deployment.yml b/examples/guardrails/github/folder-factory/.github/workflows/terraform-deployment.yml new file mode 100644 index 0000000..70f88cb --- /dev/null +++ b/examples/guardrails/github/folder-factory/.github/workflows/terraform-deployment.yml @@ -0,0 +1,91 @@ +# 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 +# +# 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. + +name: 'Cloud Deployment' + +on: + push: + branches: + - main + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve + + diff --git a/examples/guardrails/github/folder-factory/.gitignore b/examples/guardrails/github/folder-factory/.gitignore new file mode 100644 index 0000000..a5d8c0e --- /dev/null +++ b/examples/guardrails/github/folder-factory/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.terraform +.terraform.lock.hcl diff --git a/examples/guardrails/github/folder-factory/README.md b/examples/guardrails/github/folder-factory/README.md new file mode 100644 index 0000000..a139948 --- /dev/null +++ b/examples/guardrails/github/folder-factory/README.md @@ -0,0 +1,59 @@ +# Folder Factory + +This is a template for a DevOps folder factory. + +It can be used with [https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory](https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory) and is intended to house the folder configurations: + +![Screenshot 2022-05-10 12 00 19 PM](https://user-images.githubusercontent.com/94000358/169809437-aaa8538e-3ffc-48b3-9028-84e4995de150.png) + +Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: + +Secret configuration + +or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: +``` +env: + STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' + # The workload identity provider that should be used for this repository. + SERVICE_ACCOUNT: 'XXXX@XXXX' + # The service account that should be used for this repository. +``` + +## Setting up folders + +The folder factory will: +- create a folders with defined organisational policies + +It uses YAML configuration files for every folder with the following sample structure: +``` +parent: folders/XXXXXXXXX +org_policies: + policy_boolean: + constraints/compute.disableGuestAttributesAccess: true + constraints/iam.disableServiceAccountCreation: false + constraints/iam.disableServiceAccountKeyCreation: false + constraints/iam.disableServiceAccountKeyUpload: false + constraints/gcp.disableCloudLogging: false + policy_list: + constraints/compute.vmExternalIpAccess: + inherit_from_parent: null + status: true + suggested_value: null + values: +iam: + roles/resourcemanager.projectCreator: + - serviceAccount:XXXXX@XXXXXX +``` + +Every folder is defined with its own yaml file located in the following [Folder](data/folder). diff --git a/examples/guardrails/github/folder-factory/data/folders/folder.yaml.sample b/examples/guardrails/github/folder-factory/data/folders/folder.yaml.sample new file mode 100644 index 0000000..9ea745d --- /dev/null +++ b/examples/guardrails/github/folder-factory/data/folders/folder.yaml.sample @@ -0,0 +1,31 @@ +# 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 +# +# 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. + +parent: folders/01234567890 +org_policies: + policy_boolean: + constraints/compute.disableGuestAttributesAccess: true + constraints/iam.disableServiceAccountCreation: false + constraints/iam.disableServiceAccountKeyCreation: false + constraints/iam.disableServiceAccountKeyUpload: false + constraints/gcp.disableCloudLogging: false + policy_list: + constraints/compute.vmExternalIpAccess: + inherit_from_parent: null + status: true + suggested_value: null + values: +iam: + roles/resourcemanager.projectCreator: + - serviceAccount:service-account@project-xyz.iam.gserviceaccount.com \ No newline at end of file diff --git a/examples/guardrails/github/folder-factory/main.tf b/examples/guardrails/github/folder-factory/main.tf new file mode 100644 index 0000000..d024c17 --- /dev/null +++ b/examples/guardrails/github/folder-factory/main.tf @@ -0,0 +1,32 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + folders = { + for f in fileset("./data/folders", "**/*.yaml") : + trimsuffix(f, ".yaml") => yamldecode(file("./data/folders/${f}")) + } +} + +module "folder" { + source = "./modules/folder" + for_each = local.folders + name = each.key + parent = each.value.parent + policy_boolean = try(each.value.org_policies.policy_boolean, {}) + policy_list = try(each.value.org_policies.policy_list, {}) + iam = try(each.value.iam, {}) +} \ No newline at end of file diff --git a/examples/guardrails/github/folder-factory/modules/folder/README.md b/examples/guardrails/github/folder-factory/modules/folder/README.md new file mode 100644 index 0000000..4f6898b --- /dev/null +++ b/examples/guardrails/github/folder-factory/modules/folder/README.md @@ -0,0 +1,299 @@ +# Google Cloud Folder Module + +This module allows the creation and management of folders, including support for IAM bindings, organization policies, and hierarchical firewall rules. + +## Examples + +### IAM bindings + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + group_iam = { + "cloud-owners@example.org" = [ + "roles/owner", + "roles/resourcemanager.projectCreator" + ] + } + iam = { + "roles/owner" = ["user:one@example.com"] + } +} +# tftest modules=1 resources=3 +``` + +### Organization policies + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + policy_boolean = { + "constraints/compute.disableGuestAttributesAccess" = true + "constraints/compute.skipDefaultNetworkCreation" = true + } + policy_list = { + "constraints/compute.trustedImageProjects" = { + inherit_from_parent = null + suggested_value = null + status = true + values = ["projects/my-project"] + } + } +} +# tftest modules=1 resources=4 +``` + +### Firewall policy factory + +In the same way as for the [organization](../organization) module, the in-built factory allows you to define a single policy, using one file for rules, and an optional file for CIDR range substitution variables. Remember that non-absolute paths are relative to the root module (the folder where you run `terraform`). + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + firewall_policy_factory = { + cidr_file = "data/cidrs.yaml" + policy_name = null + rules_file = "data/rules.yaml" + } + firewall_policy_association = { + factory-policy = module.folder.firewall_policy_id["factory"] + } +} +# tftest skip +``` + +```yaml +# cidrs.yaml + +rfc1918: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 +``` + +```yaml +# rules.yaml + +allow-admins: + description: Access from the admin subnet to all subnets + direction: INGRESS + action: allow + priority: 1000 + ranges: + - $rfc1918 + ports: + all: [] + target_resources: null + enable_logging: false + +allow-ssh-from-iap: + description: Enable SSH from IAP + direction: INGRESS + action: allow + priority: 1002 + ranges: + - 35.235.240.0/20 + ports: + tcp: ["22"] + target_resources: null + enable_logging: false +``` + +### Logging Sinks + +```hcl +module "gcs" { + source = "./modules/gcs" + project_id = "my-project" + name = "gcs_sink" + force_destroy = true +} + +module "dataset" { + source = "./modules/bigquery-dataset" + project_id = "my-project" + id = "bq_sink" +} + +module "pubsub" { + source = "./modules/pubsub" + project_id = "my-project" + name = "pubsub_sink" +} + +module "bucket" { + source = "./modules/logging-bucket" + parent_type = "project" + parent = "my-project" + id = "bucket" +} + +module "folder-sink" { + source = "./modules/folder" + parent = "folders/657104291943" + name = "my-folder" + logging_sinks = { + warnings = { + type = "storage" + destination = module.gcs.name + filter = "severity=WARNING" + include_children = true + exclusions = {} + } + info = { + type = "bigquery" + destination = module.dataset.id + filter = "severity=INFO" + include_children = true + exclusions = {} + } + notice = { + type = "pubsub" + destination = module.pubsub.id + filter = "severity=NOTICE" + include_children = true + exclusions = {} + } + debug = { + type = "logging" + destination = module.bucket.id + filter = "severity=DEBUG" + include_children = true + exclusions = { + no-compute = "logName:compute" + } + } + } + logging_exclusions = { + no-gce-instances = "resource.type=gce_instance" + } +} +# tftest modules=5 resources=14 +``` + +### Hierarchical firewall policies + +```hcl +module "folder1" { + source = "./modules/folder" + parent = var.organization_id + name = "policy-container" + + firewall_policies = { + iap-policy = { + allow-iap-ssh = { + description = "Always allow ssh from IAP" + direction = "INGRESS" + action = "allow" + priority = 100 + ranges = ["35.235.240.0/20"] + ports = { tcp = ["22"] } + target_service_accounts = null + target_resources = null + logging = false + } + } + } + firewall_policy_association = { + iap-policy = "iap-policy" + } +} + +module "folder2" { + source = "./modules/folder" + parent = var.organization_id + name = "hf2" + firewall_policy_association = { + iap-policy = module.folder1.firewall_policy_id["iap-policy"] + } +} +# tftest modules=2 resources=6 +``` + +## 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. + +```hcl +module "org" { + source = "./modules/organization" + organization_id = var.organization_id + tags = { + environment = { + description = "Environment specification." + iam = null + values = { + dev = null + prod = null + } + } + } +} + +module "folder" { + source = "./modules/folder" + name = "Test" + parent = module.org.organization_id + tag_bindings = { + env-prod = module.org.tag_values["environment/prod"].id + foo = "tagValues/12345678" + } +} +# tftest modules=2 resources=6 +``` + + + + +## Files + +| name | description | resources | +|---|---|---| +| [firewall-policies.tf](./firewall-policies.tf) | None | google_compute_firewall_policy · google_compute_firewall_policy_association · google_compute_firewall_policy_rule | +| [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_folder_iam_binding | +| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_folder_exclusion · google_logging_folder_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | +| [main.tf](./main.tf) | Module-level locals and resources. | google_essential_contacts_contact · google_folder | +| [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_folder_organization_policy | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding | +| [variables.tf](./variables.tf) | Module variables. | | +| [versions.tf](./versions.tf) | Version pins. | | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | +| [firewall_policies](variables.tf#L24) | Hierarchical firewall policies created in this folder. | map(map(object({…}))) | | {} | +| [firewall_policy_association](variables.tf#L41) | The hierarchical firewall policy to associate to this folder. Must be either a key in the `firewall_policies` map or the id of a policy defined somewhere else. | map(string) | | {} | +| [firewall_policy_factory](variables.tf#L48) | Configuration for the firewall policy factory. | object({…}) | | null | +| [folder_create](variables.tf#L58) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | +| [group_iam](variables.tf#L64) | 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. | map(list(string)) | | {} | +| [iam](variables.tf#L71) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [id](variables.tf#L78) | Folder ID in case you use folder_create=false. | string | | null | +| [logging_exclusions](variables.tf#L84) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L91) | Logging sinks to create for this folder. | map(object({…})) | | {} | +| [name](variables.tf#L112) | Folder name. | string | | null | +| [parent](variables.tf#L118) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [policy_boolean](variables.tf#L128) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | +| [policy_list](variables.tf#L135) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L147) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [firewall_policies](outputs.tf#L16) | Map of firewall policy resources created in this folder. | | +| [firewall_policy_id](outputs.tf#L21) | Map of firewall policy ids created in this folder. | | +| [folder](outputs.tf#L26) | Folder resource. | | +| [id](outputs.tf#L31) | Folder id. | | +| [name](outputs.tf#L41) | Folder name. | | +| [sink_writer_identities](outputs.tf#L46) | Writer identities created for each sink. | | + + diff --git a/examples/guardrails/github/folder-factory/modules/folder/firewall-policies.tf b/examples/guardrails/github/folder-factory/modules/folder/firewall-policies.tf new file mode 100644 index 0000000..96224c5 --- /dev/null +++ b/examples/guardrails/github/folder-factory/modules/folder/firewall-policies.tf @@ -0,0 +1,93 @@ +/** + * 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 + * + * 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 { + _factory_cidrs = try( + yamldecode(file(var.firewall_policy_factory.cidr_file)), {} + ) + _factory_name = ( + try(var.firewall_policy_factory.policy_name, null) == null + ? "factory" + : var.firewall_policy_factory.policy_name + ) + _factory_rules = try( + yamldecode(file(var.firewall_policy_factory.rules_file)), {} + ) + _factory_rules_parsed = { + for name, rule in local._factory_rules : name => merge(rule, { + ranges = flatten([ + for r in(rule.ranges == null ? [] : rule.ranges) : + lookup(local._factory_cidrs, trimprefix(r, "$"), r) + ]) + }) + } + _merged_rules = flatten([ + for policy, rules in local.firewall_policies : [ + for name, rule in rules : merge(rule, { + policy = policy + name = name + }) + ] + ]) + firewall_policies = merge(var.firewall_policies, ( + length(local._factory_rules) == 0 + ? {} + : { (local._factory_name) = local._factory_rules_parsed } + )) + firewall_rules = { + for r in local._merged_rules : "${r.policy}-${r.name}" => r + } +} + +resource "google_compute_firewall_policy" "policy" { + for_each = local.firewall_policies + short_name = each.key + parent = local.folder.id +} + +resource "google_compute_firewall_policy_rule" "rule" { + for_each = local.firewall_rules + firewall_policy = google_compute_firewall_policy.policy[each.value.policy].id + action = each.value.action + direction = each.value.direction + priority = try(each.value.priority, null) + target_resources = try(each.value.target_resources, null) + target_service_accounts = try(each.value.target_service_accounts, null) + enable_logging = try(each.value.logging, null) + # preview = each.value.preview + description = each.value.description + match { + src_ip_ranges = each.value.direction == "INGRESS" ? each.value.ranges : null + dest_ip_ranges = each.value.direction == "EGRESS" ? each.value.ranges : null + dynamic "layer4_configs" { + for_each = each.value.ports + iterator = port + content { + ip_protocol = port.key + ports = port.value + } + } + } +} + + +resource "google_compute_firewall_policy_association" "association" { + for_each = var.firewall_policy_association + name = replace(local.folder.id, "/", "-") + attachment_target = local.folder.id + firewall_policy = try(google_compute_firewall_policy.policy[each.value].id, each.value) +} + diff --git a/examples/guardrails/github/folder-factory/modules/folder/iam.tf b/examples/guardrails/github/folder-factory/modules/folder/iam.tf new file mode 100644 index 0000000..52886ba --- /dev/null +++ b/examples/guardrails/github/folder-factory/modules/folder/iam.tf @@ -0,0 +1,40 @@ +/** + * 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 + * + * 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 IAM bindings, roles and audit logging resources. + +locals { + group_iam_roles = distinct(flatten(values(var.group_iam))) + group_iam = { + for r in local.group_iam_roles : r => [ + for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null + ] + } + iam = { + for role in distinct(concat(keys(var.iam), keys(local.group_iam))) : + role => concat( + try(var.iam[role], []), + try(local.group_iam[role], []) + ) + } +} + +resource "google_folder_iam_binding" "authoritative" { + for_each = local.iam + folder = local.folder.name + role = each.key + members = each.value +} diff --git a/examples/guardrails/github/folder-factory/modules/folder/logging.tf b/examples/guardrails/github/folder-factory/modules/folder/logging.tf new file mode 100644 index 0000000..d6a195e --- /dev/null +++ b/examples/guardrails/github/folder-factory/modules/folder/logging.tf @@ -0,0 +1,91 @@ +/** + * 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 + * + * 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 Log sinks and supporting resources. + +locals { + sink_bindings = { + for type in ["bigquery", "pubsub", "logging", "storage"] : + type => { + for name, sink in var.logging_sinks : + name => sink + if sink.type == type + } + } +} + +resource "google_logging_folder_sink" "sink" { + for_each = var.logging_sinks + name = each.key + #description = "${each.key} (Terraform-managed)." + folder = local.folder.name + destination = "${each.value.type}.googleapis.com/${each.value.destination}" + filter = each.value.filter + include_children = each.value.include_children + + dynamic "exclusions" { + for_each = each.value.exclusions + iterator = exclusion + content { + name = exclusion.key + filter = exclusion.value + } + } + + depends_on = [ + google_folder_iam_binding.authoritative + ] +} + +resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { + for_each = local.sink_bindings["storage"] + bucket = each.value.destination + role = "roles/storage.objectCreator" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" { + for_each = local.sink_bindings["bigquery"] + project = split("/", each.value.destination)[1] + dataset_id = split("/", each.value.destination)[3] + role = "roles/bigquery.dataEditor" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" { + for_each = local.sink_bindings["pubsub"] + project = split("/", each.value.destination)[1] + topic = split("/", each.value.destination)[3] + role = "roles/pubsub.publisher" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_project_iam_member" "bucket-sinks-binding" { + for_each = local.sink_bindings["logging"] + project = split("/", each.value.destination)[1] + role = "roles/logging.bucketWriter" + member = google_logging_folder_sink.sink[each.key].writer_identity + # TODO(jccb): use a condition to limit writer-identity only to this + # bucket +} + +resource "google_logging_folder_exclusion" "logging-exclusion" { + for_each = var.logging_exclusions + name = each.key + folder = local.folder.name + description = "${each.key} (Terraform-managed)." + filter = each.value +} diff --git a/examples/guardrails/github/folder-factory/modules/folder/main.tf b/examples/guardrails/github/folder-factory/modules/folder/main.tf new file mode 100644 index 0000000..5d285d2 --- /dev/null +++ b/examples/guardrails/github/folder-factory/modules/folder/main.tf @@ -0,0 +1,43 @@ +/** + * 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 + * + * 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 { + folder = ( + var.folder_create + ? try(google_folder.folder.0, null) + : try(data.google_folder.folder.0, null) + ) +} + +data "google_folder" "folder" { + count = var.folder_create ? 0 : 1 + folder = var.id +} + +resource "google_folder" "folder" { + count = var.folder_create ? 1 : 0 + display_name = var.name + parent = var.parent +} + +resource "google_essential_contacts_contact" "contact" { + provider = google-beta + for_each = var.contacts + parent = local.folder.name + email = each.key + language_tag = "en" + notification_category_subscriptions = each.value +} diff --git a/examples/guardrails/github/folder-factory/modules/folder/organization-policies.tf b/examples/guardrails/github/folder-factory/modules/folder/organization-policies.tf new file mode 100644 index 0000000..177a3d8 --- /dev/null +++ b/examples/guardrails/github/folder-factory/modules/folder/organization-policies.tf @@ -0,0 +1,90 @@ +/** + * 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 + * + * 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 Folder-level organization policies. + +resource "google_folder_organization_policy" "boolean" { + for_each = var.policy_boolean + folder = local.folder.name + constraint = each.key + + dynamic "boolean_policy" { + for_each = each.value == null ? [] : [each.value] + iterator = policy + content { + enforced = policy.value + } + } + + dynamic "restore_policy" { + for_each = each.value == null ? [""] : [] + content { + default = true + } + } +} + +resource "google_folder_organization_policy" "list" { + for_each = var.policy_list + folder = local.folder.name + constraint = each.key + + dynamic "list_policy" { + for_each = each.value.status == null ? [] : [each.value] + iterator = policy + content { + inherit_from_parent = policy.value.inherit_from_parent + suggested_value = policy.value.suggested_value + dynamic "allow" { + for_each = policy.value.status ? [""] : [] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + dynamic "deny" { + for_each = policy.value.status ? [] : [""] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + } + } + + dynamic "restore_policy" { + for_each = each.value.status == null ? [true] : [] + content { + default = true + } + } +} diff --git a/examples/guardrails/github/folder-factory/modules/folder/outputs.tf b/examples/guardrails/github/folder-factory/modules/folder/outputs.tf new file mode 100644 index 0000000..37babc6 --- /dev/null +++ b/examples/guardrails/github/folder-factory/modules/folder/outputs.tf @@ -0,0 +1,51 @@ +/** + * 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 + * + * 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. + */ +output "firewall_policies" { + description = "Map of firewall policy resources created in this folder." + value = { for k, v in google_compute_firewall_policy.policy : k => v } +} + +output "firewall_policy_id" { + description = "Map of firewall policy ids created in this folder." + value = { for k, v in google_compute_firewall_policy.policy : k => v.id } +} + +output "folder" { + description = "Folder resource." + value = local.folder +} + +output "id" { + description = "Folder id." + value = local.folder.name + depends_on = [ + google_folder_iam_binding.authoritative, + google_folder_organization_policy.boolean, + google_folder_organization_policy.list + ] +} + +output "name" { + description = "Folder name." + value = local.folder.display_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 + } +} diff --git a/examples/guardrails/github/folder-factory/modules/folder/tags.tf b/examples/guardrails/github/folder-factory/modules/folder/tags.tf new file mode 100644 index 0000000..2cd2f2f --- /dev/null +++ b/examples/guardrails/github/folder-factory/modules/folder/tags.tf @@ -0,0 +1,21 @@ +/** + * 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 + * + * 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. + */ + +resource "google_tags_tag_binding" "binding" { + for_each = coalesce(var.tag_bindings, {}) + parent = "//cloudresourcemanager.googleapis.com/${local.folder.id}" + tag_value = each.value +} diff --git a/examples/guardrails/github/folder-factory/modules/folder/variables.tf b/examples/guardrails/github/folder-factory/modules/folder/variables.tf new file mode 100644 index 0000000..a3f32e3 --- /dev/null +++ b/examples/guardrails/github/folder-factory/modules/folder/variables.tf @@ -0,0 +1,151 @@ +/** + * 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 + * + * 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. + */ + +variable "contacts" { + description = "List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES." + type = map(list(string)) + default = {} + nullable = false +} + +variable "firewall_policies" { + description = "Hierarchical firewall policies created in this folder." + type = map(map(object({ + action = string + description = string + direction = string + logging = bool + ports = map(list(string)) + priority = number + ranges = list(string) + target_resources = list(string) + target_service_accounts = list(string) + }))) + default = {} + nullable = false +} + +variable "firewall_policy_association" { + description = "The hierarchical firewall policy to associate to this folder. Must be either a key in the `firewall_policies` map or the id of a policy defined somewhere else." + type = map(string) + default = {} + nullable = false +} + +variable "firewall_policy_factory" { + description = "Configuration for the firewall policy factory." + type = object({ + cidr_file = string + policy_name = string + rules_file = string + }) + default = null +} + +variable "folder_create" { + description = "Create folder. When set to false, uses id to reference an existing folder." + type = bool + default = true +} + +variable "group_iam" { + description = "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." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam" { + description = "IAM bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "id" { + description = "Folder ID in case you use folder_create=false." + type = string + default = null +} + +variable "logging_exclusions" { + description = "Logging exclusions for this folder in the form {NAME -> FILTER}." + type = map(string) + default = {} + nullable = false +} + +variable "logging_sinks" { + description = "Logging sinks to create for this folder." + type = map(object({ + destination = string + type = string + filter = string + include_children = bool + # TODO exclusions also support description and disabled + exclusions = map(string) + })) + validation { + condition = alltrue([ + for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } + default = {} + nullable = false +} + +variable "name" { + description = "Folder name." + type = string + default = null +} + +variable "parent" { + description = "Parent in folders/folder_id or organizations/org_id format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "policy_boolean" { + description = "Map of boolean org policies and enforcement value, set value to null for policy restore." + type = map(bool) + default = {} + nullable = false +} + +variable "policy_list" { + description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." + type = map(object({ + inherit_from_parent = bool + suggested_value = string + status = bool + values = list(string) + })) + default = {} + nullable = false +} + +variable "tag_bindings" { + description = "Tag bindings for this folder, in key => tag value id format." + type = map(string) + default = null +} diff --git a/examples/guardrails/github/folder-factory/modules/folder/versions.tf b/examples/guardrails/github/folder-factory/modules/folder/versions.tf new file mode 100644 index 0000000..e72a780 --- /dev/null +++ b/examples/guardrails/github/folder-factory/modules/folder/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.1.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/github/folder-factory/outputs.tf b/examples/guardrails/github/folder-factory/outputs.tf new file mode 100644 index 0000000..a84b9d5 --- /dev/null +++ b/examples/guardrails/github/folder-factory/outputs.tf @@ -0,0 +1,20 @@ +/** + * 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 + * + * 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. + */ + +output "folders" { + description = "Created folders." + value = module.folder +} \ No newline at end of file diff --git a/examples/guardrails/github/folder-factory/provider.tf b/examples/guardrails/github/folder-factory/provider.tf new file mode 100644 index 0000000..0e17ef9 --- /dev/null +++ b/examples/guardrails/github/folder-factory/provider.tf @@ -0,0 +1,28 @@ +/** + * 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 + * + * 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. + */ + +terraform { + backend "gcs" { + } +} + +provider "google" { + +} + +provider "google-beta" { + +} diff --git a/examples/guardrails/github/folder-factory/variables.tf b/examples/guardrails/github/folder-factory/variables.tf new file mode 100644 index 0000000..156a5ac --- /dev/null +++ b/examples/guardrails/github/folder-factory/variables.tf @@ -0,0 +1,15 @@ +/** + * 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 + * + * 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. + */ \ No newline at end of file diff --git a/examples/guardrails/github/project-factory/.github/workflows/terraform-deployment.yml b/examples/guardrails/github/project-factory/.github/workflows/terraform-deployment.yml new file mode 100644 index 0000000..4099678 --- /dev/null +++ b/examples/guardrails/github/project-factory/.github/workflows/terraform-deployment.yml @@ -0,0 +1,94 @@ +# 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 +# +# 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. + +name: 'Cloud Deployment' + +on: + push: + branches: + - main + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + FOLDER: ${{ secrets.FOLDER }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} + +# OR SET MANUALLY +# +#env: +# STATE_BUCKET: 'XXXX' +# FOLDER: 'folders/XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan -var "folder=$FOLDER" + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -var "folder=$FOLDER" -auto-approve + + diff --git a/examples/guardrails/github/project-factory/.gitignore b/examples/guardrails/github/project-factory/.gitignore new file mode 100644 index 0000000..a5d8c0e --- /dev/null +++ b/examples/guardrails/github/project-factory/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.terraform +.terraform.lock.hcl diff --git a/examples/guardrails/github/project-factory/README.md b/examples/guardrails/github/project-factory/README.md new file mode 100644 index 0000000..f901e7f --- /dev/null +++ b/examples/guardrails/github/project-factory/README.md @@ -0,0 +1,75 @@ +# Project Factory + +This is a template for a DevOps project factory. + +It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory) and is intended to house the projects of a specified folder: + +Overview + +Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. + +![Folder Factory](https://user-images.githubusercontent.com/94000358/169809882-f5ff9fb1-d037-49de-8c2c-bf0d457b662f.png) + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: + +Secret settings + +or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: +``` +env: + STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + FOLDER: 'folders/XXXX' + # The folder under which the projects should be created + WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' + # The workload identity provider that should be used for this repository. + SERVICE_ACCOUNT: 'XXXX@XXXX' + # The service account that should be used for this repository. +``` + +## Setting up projects + +The project factory will: +- create a service account with defined rights +- create a project within the folder +- connect the service account to the Github repository informantion + +It uses YAML configuration files for every project with the following sample structure: +``` +billing_account_id: XXXXXX-XXXXXX-XXXXXX +roles: + - roles/viewer + - roles/iam.serviceAccountUser + - roles/iam.securityReviewer + - roles/monitoring.viewer + - roles/monitoring.editor + - roles/monitoring.alertPolicyViewer + - roles/monitoring.alertPolicyEditor + - roles/monitoring.dashboardViewer + - roles/monitoring.dashboardEditor + - roles/monitoring.notificationChannelViewer + - roles/monitoring.notificationChannelEditor + - roles/monitoring.servicesViewer + - roles/monitoring.servicesEditor + - roles/monitoring.uptimeCheckConfigViewer + - roles/monitoring.uptimeCheckConfigEditor + - roles/secretmanager.viewer + - roles/secretmanager.secretVersionManager + - roles/secretmanager.admin + - roles/storage.admin + - roles/storage.objectAdmin + - roles/storage.objectCreator + - roles/storage.objectViewer +repo_provider: github +repo_name: devops-governance/skunkworks +repo_branch: dev +``` + +Every project is defined with its own file located in the [Project Folder](data/projects). diff --git a/examples/guardrails/github/project-factory/data/projects/project.yaml.sample b/examples/guardrails/github/project-factory/data/projects/project.yaml.sample new file mode 100644 index 0000000..d813f4b --- /dev/null +++ b/examples/guardrails/github/project-factory/data/projects/project.yaml.sample @@ -0,0 +1,41 @@ +# 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 +# +# 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. + +billing_account_id: XXXXXX-XXXXXX-XXXXXX +roles: + - roles/viewer + - roles/iam.serviceAccountUser + - roles/iam.securityReviewer + - roles/monitoring.viewer + - roles/monitoring.editor + - roles/monitoring.alertPolicyViewer + - roles/monitoring.alertPolicyEditor + - roles/monitoring.dashboardViewer + - roles/monitoring.dashboardEditor + - roles/monitoring.notificationChannelViewer + - roles/monitoring.notificationChannelEditor + - roles/monitoring.servicesViewer + - roles/monitoring.servicesEditor + - roles/monitoring.uptimeCheckConfigViewer + - roles/monitoring.uptimeCheckConfigEditor + - roles/secretmanager.viewer + - roles/secretmanager.secretVersionManager + - roles/secretmanager.admin + - roles/storage.admin + - roles/storage.objectAdmin + - roles/storage.objectCreator + - roles/storage.objectViewer +repo_provider: github +repo_name: github-org/github-repo +repo_branch: dev diff --git a/examples/guardrails/github/project-factory/main.tf b/examples/guardrails/github/project-factory/main.tf new file mode 100644 index 0000000..3d589d4 --- /dev/null +++ b/examples/guardrails/github/project-factory/main.tf @@ -0,0 +1,35 @@ +/** + * 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 + * + * 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 { + projects = { + for f in fileset("./data/projects", "**/*.yaml") : + trimsuffix(f, ".yaml") => yamldecode(file("./data/projects/${f}")) + } +} + +module "project" { + source = "./modules/project_plus" + for_each = local.projects + team = each.key + repo_sub = "${each.value.repo_provider == "gitlab" ? "project_path:${each.value.repo_name}:ref_type:branch:ref:${each.value.repo_branch}" : "repo:${each.value.repo_name}:ref:refs/heads/${each.value.repo_branch}"}" + repo_provider = each.value.repo_provider + billing_account = each.value.billing_account_id + folder = var.folder + roles = try(each.value.roles, []) + wif-pool = "${each.value.repo_provider == "gitlab" ? google_iam_workload_identity_pool.wif-pool-gitlab.name : google_iam_workload_identity_pool.wif-pool-github.name}" + depends_on = [google_iam_workload_identity_pool.wif-pool-github,google_iam_workload_identity_pool.wif-pool-gitlab] +} diff --git a/examples/guardrails/github/project-factory/modules/project/README.md b/examples/guardrails/github/project-factory/modules/project/README.md new file mode 100644 index 0000000..e4f2139 --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project/README.md @@ -0,0 +1,308 @@ +# Project Module + +## Examples + +### Minimal example with IAM + +```hcl +locals { + gke_service_account = "my_gke_service_account" +} + +module "project" { + source = "./modules/project" + billing_account = "123456-123456-123456" + name = "project-example" + parent = "folders/1234567890" + prefix = "foo" + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + iam = { + "roles/container.hostServiceAgentUser" = [ + "serviceAccount:${local.gke_service_account}" + ] + } +} +# tftest modules=1 resources=4 +``` + +### Minimal example with IAM additive roles + +```hcl +module "project" { + source = "./modules/project" + name = "project-example" + + iam_additive = { + "roles/viewer" = [ + "group:one@example.org", "group:two@xample.org" + ], + "roles/storage.objectAdmin" = [ + "group:two@example.org" + ], + "roles/owner" = [ + "group:three@example.org" + ], + } +} +# tftest modules=1 resources=5 +``` + +### Shared VPC service + +```hcl +module "project" { + source = "./modules/project" + name = "project-example" + + shared_vpc_service_config = { + attach = true + host_project = "my-host-project" + service_identity_iam = { + "roles/compute.networkUser" = [ + "cloudservices", "container-engine" + ] + "roles/vpcaccess.user" = [ + "cloudrun" + ] + "roles/container.hostServiceAgentUser" = [ + "container-engine" + ] + } + } +} +# tftest modules=1 resources=6 +``` + +### Organization policies + +```hcl +module "project" { + source = "./modules/project" + billing_account = "123456-123456-123456" + name = "project-example" + parent = "folders/1234567890" + prefix = "foo" + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + policy_boolean = { + "constraints/compute.disableGuestAttributesAccess" = true + "constraints/compute.skipDefaultNetworkCreation" = true + } + policy_list = { + "constraints/compute.trustedImageProjects" = { + inherit_from_parent = null + suggested_value = null + status = true + values = ["projects/my-project"] + } + } +} +# tftest modules=1 resources=6 +``` + +## Logging Sinks + +```hcl +module "gcs" { + source = "./modules/gcs" + project_id = var.project_id + name = "gcs_sink" + force_destroy = true +} + +module "dataset" { + source = "./modules/bigquery-dataset" + project_id = var.project_id + id = "bq_sink" +} + +module "pubsub" { + source = "./modules/pubsub" + project_id = var.project_id + name = "pubsub_sink" +} + +module "bucket" { + source = "./modules/logging-bucket" + parent_type = "project" + parent = "my-project" + id = "bucket" +} + +module "project-host" { + source = "./modules/project" + name = "my-project" + billing_account = "123456-123456-123456" + parent = "folders/1234567890" + logging_sinks = { + warnings = { + type = "storage" + destination = module.gcs.name + filter = "severity=WARNING" + iam = false + unique_writer = false + exclusions = {} + } + info = { + type = "bigquery" + destination = module.dataset.id + filter = "severity=INFO" + iam = false + unique_writer = false + exclusions = {} + } + notice = { + type = "pubsub" + destination = module.pubsub.id + filter = "severity=NOTICE" + iam = true + unique_writer = false + exclusions = {} + } + debug = { + type = "logging" + destination = module.bucket.id + filter = "severity=DEBUG" + iam = true + unique_writer = false + exclusions = { + no-compute = "logName:compute" + } + } + } + logging_exclusions = { + no-gce-instances = "resource.type=gce_instance" + } +} +# tftest modules=5 resources=12 +``` + +## Cloud KMS encryption keys + +```hcl +module "project" { + source = "./modules/project" + name = "my-project" + billing_account = "123456-123456-123456" + prefix = "foo" + services = [ + "compute.googleapis.com", + "storage.googleapis.com" + ] + service_encryption_key_ids = { + compute = [ + "projects/kms-central-prj/locations/europe-west3/keyRings/my-keyring/cryptoKeys/europe3-gce", + "projects/kms-central-prj/locations/europe-west4/keyRings/my-keyring/cryptoKeys/europe4-gce" + ] + storage = [ + "projects/kms-central-prj/locations/europe/keyRings/my-keyring/cryptoKeys/europe-gcs" + ] + } +} +# tftest modules=1 resources=7 +``` + +## 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. + +```hcl +module "org" { + source = "./modules/organization" + organization_id = var.organization_id + tags = { + environment = { + description = "Environment specification." + iam = null + values = { + dev = null + prod = null + } + } + } +} + +module "project" { + source = "./modules/project" + name = "test-project" + tag_bindings = { + env-prod = module.org.tag_values["environment/prod"].id + foo = "tagValues/12345678" + } +} +# tftest modules=2 resources=6 +``` + + + + +## Files + +| name | description | resources | +|---|---|---| +| [iam.tf](./iam.tf) | Generic and OSLogin-specific IAM bindings and roles. | google_project_iam_binding · google_project_iam_custom_role · google_project_iam_member | +| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_project_exclusion · google_logging_project_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | +| [main.tf](./main.tf) | Module-level locals and resources. | google_compute_project_metadata_item · google_essential_contacts_contact · google_monitoring_monitored_project · google_project · google_project_service · google_resource_manager_lien | +| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_project_organization_policy | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [service-accounts.tf](./service-accounts.tf) | Service identities and supporting resources. | google_kms_crypto_key_iam_member · google_project_service_identity | +| [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project · google_compute_shared_vpc_service_project · google_project_iam_member | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding | +| [variables.tf](./variables.tf) | Module variables. | | +| [versions.tf](./versions.tf) | Version pins. | | +| [vpc-sc.tf](./vpc-sc.tf) | VPC-SC project-level perimeter configuration. | google_access_context_manager_service_perimeter_resource | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [name](variables.tf#L125) | Project name and id suffix. | string | ✓ | | +| [auto_create_network](variables.tf#L17) | Whether to create the default network for the project. | bool | | false | +| [billing_account](variables.tf#L23) | Billing account id. | string | | null | +| [contacts](variables.tf#L29) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | +| [custom_roles](variables.tf#L36) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| [descriptive_name](variables.tf#L43) | Name of the project name. Used for project name instead of `name` variable. | string | | null | +| [group_iam](variables.tf#L49) | 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. | map(list(string)) | | {} | +| [iam](variables.tf#L56) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive](variables.tf#L63) | IAM additive bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive_members](variables.tf#L70) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | map(list(string)) | | {} | +| [labels](variables.tf#L76) | Resource labels. | map(string) | | {} | +| [lien_reason](variables.tf#L83) | If non-empty, creates a project lien with this description. | string | | "" | +| [logging_exclusions](variables.tf#L89) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L96) | Logging sinks to create for this project. | map(object({…})) | | {} | +| [metric_scopes](variables.tf#L118) | List of projects that will act as metric scopes for this project. | list(string) | | [] | +| [oslogin](variables.tf#L130) | Enable OS Login. | bool | | false | +| [oslogin_admins](variables.tf#L136) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | +| [oslogin_users](variables.tf#L144) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | +| [parent](variables.tf#L151) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [policy_boolean](variables.tf#L161) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | +| [policy_list](variables.tf#L168) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | +| [prefix](variables.tf#L180) | Prefix used to generate project id and name. | string | | null | +| [project_create](variables.tf#L186) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | +| [service_config](variables.tf#L192) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L204) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | +| [service_perimeter_bridges](variables.tf#L211) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | +| [service_perimeter_standard](variables.tf#L218) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | +| [services](variables.tf#L224) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L230) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L239) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | +| [skip_delete](variables.tf#L249) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [tag_bindings](variables.tf#L255) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [custom_roles](outputs.tf#L17) | Ids of the created custom roles. | | +| [name](outputs.tf#L25) | Project name. | | +| [number](outputs.tf#L38) | Project number. | | +| [project_id](outputs.tf#L51) | Project id. | | +| [service_accounts](outputs.tf#L66) | Product robot service accounts in project. | | +| [sink_writer_identities](outputs.tf#L82) | Writer identities created for each sink. | | + + diff --git a/examples/guardrails/github/project-factory/modules/project/iam.tf b/examples/guardrails/github/project-factory/modules/project/iam.tf new file mode 100644 index 0000000..69925cc --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project/iam.tf @@ -0,0 +1,115 @@ +/** + * 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 + * + * 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 Generic and OSLogin-specific IAM bindings and roles. + +# IAM notes: +# - external users need to have accepted the invitation email to join +# - oslogin roles also require role to list instances +# - additive (non-authoritative) roles might fail due to dynamic values + +locals { + _group_iam_roles = distinct(flatten(values(var.group_iam))) + _group_iam = { + for r in local._group_iam_roles : r => [ + 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( + try(var.iam[role], []), + 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_project_iam_custom_role" "roles" { + for_each = var.custom_roles + project = local.project.project_id + role_id = each.key + title = "Custom role ${each.key}" + description = "Terraform-managed." + permissions = each.value +} + +resource "google_project_iam_binding" "authoritative" { + for_each = local.iam + project = local.project.project_id + role = each.key + members = each.value + depends_on = [ + google_project_service.project_services, + google_project_iam_custom_role.roles + ] +} + +resource "google_project_iam_member" "additive" { + for_each = ( + length(var.iam_additive) + length(var.iam_additive_members) > 0 + ? local.iam_additive + : {} + ) + project = local.project.project_id + role = each.value.role + member = each.value.member + depends_on = [ + google_project_service.project_services, + google_project_iam_custom_role.roles + ] +} + +resource "google_project_iam_member" "oslogin_iam_serviceaccountuser" { + for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([]) + project = local.project.project_id + role = "roles/iam.serviceAccountUser" + member = each.value +} + +resource "google_project_iam_member" "oslogin_compute_viewer" { + for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([]) + project = local.project.project_id + role = "roles/compute.viewer" + member = each.value +} + +resource "google_project_iam_member" "oslogin_admins" { + for_each = var.oslogin ? toset(var.oslogin_admins) : toset([]) + project = local.project.project_id + role = "roles/compute.osAdminLogin" + member = each.value +} + +resource "google_project_iam_member" "oslogin_users" { + for_each = var.oslogin ? toset(var.oslogin_users) : toset([]) + project = local.project.project_id + role = "roles/compute.osLogin" + member = each.value +} diff --git a/examples/guardrails/github/project-factory/modules/project/logging.tf b/examples/guardrails/github/project-factory/modules/project/logging.tf new file mode 100644 index 0000000..04d7abf --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project/logging.tf @@ -0,0 +1,91 @@ +/** + * 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 + * + * 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 Log sinks and supporting resources. + +locals { + sink_bindings = { + for type in ["bigquery", "pubsub", "logging", "storage"] : + type => { + for name, sink in var.logging_sinks : + name => sink if sink.iam && sink.type == type + } + } +} + +resource "google_logging_project_sink" "sink" { + for_each = var.logging_sinks + name = each.key + #description = "${each.key} (Terraform-managed)." + project = local.project.project_id + destination = "${each.value.type}.googleapis.com/${each.value.destination}" + filter = each.value.filter + unique_writer_identity = each.value.unique_writer + + dynamic "exclusions" { + for_each = each.value.exclusions + iterator = exclusion + content { + name = exclusion.key + filter = exclusion.value + } + } + + depends_on = [ + google_project_iam_binding.authoritative, + google_project_iam_member.additive + ] +} + +resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { + for_each = local.sink_bindings["storage"] + bucket = each.value.destination + role = "roles/storage.objectCreator" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" { + for_each = local.sink_bindings["bigquery"] + project = split("/", each.value.destination)[1] + dataset_id = split("/", each.value.destination)[3] + role = "roles/bigquery.dataEditor" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" { + for_each = local.sink_bindings["pubsub"] + project = split("/", each.value.destination)[1] + topic = split("/", each.value.destination)[3] + role = "roles/pubsub.publisher" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_project_iam_member" "bucket-sinks-binding" { + for_each = local.sink_bindings["logging"] + project = split("/", each.value.destination)[1] + role = "roles/logging.bucketWriter" + member = google_logging_project_sink.sink[each.key].writer_identity + # TODO(jccb): use a condition to limit writer-identity only to this + # bucket +} + +resource "google_logging_project_exclusion" "logging-exclusion" { + for_each = var.logging_exclusions + name = each.key + project = local.project.project_id + description = "${each.key} (Terraform-managed)." + filter = each.value +} diff --git a/examples/guardrails/github/project-factory/modules/project/main.tf b/examples/guardrails/github/project-factory/modules/project/main.tf new file mode 100644 index 0000000..9f0dff4 --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project/main.tf @@ -0,0 +1,95 @@ +/** + * 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 + * + * 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 { + descriptive_name = ( + var.descriptive_name != null ? var.descriptive_name : "${local.prefix}${var.name}" + ) + parent_type = var.parent == null ? null : split("/", var.parent)[0] + parent_id = var.parent == null ? null : split("/", var.parent)[1] + prefix = var.prefix == null ? "" : "${var.prefix}-" + project = ( + var.project_create ? + { + project_id = try(google_project.project.0.project_id, null) + number = try(google_project.project.0.number, null) + name = try(google_project.project.0.name, null) + } + : { + project_id = "${local.prefix}${var.name}" + number = try(data.google_project.project.0.number, null) + name = try(data.google_project.project.0.name, null) + } + ) +} + +data "google_project" "project" { + count = var.project_create ? 0 : 1 + project_id = "${local.prefix}${var.name}" +} + +resource "google_project" "project" { + count = var.project_create ? 1 : 0 + org_id = local.parent_type == "organizations" ? local.parent_id : null + folder_id = local.parent_type == "folders" ? local.parent_id : null + project_id = "${local.prefix}${var.name}" + name = local.descriptive_name + billing_account = var.billing_account + auto_create_network = var.auto_create_network + labels = var.labels + skip_delete = var.skip_delete +} + +resource "google_project_service" "project_services" { + for_each = toset(var.services) + project = local.project.project_id + service = each.value + disable_on_destroy = var.service_config.disable_on_destroy + disable_dependent_services = var.service_config.disable_dependent_services +} + +resource "google_compute_project_metadata_item" "oslogin_meta" { + count = var.oslogin ? 1 : 0 + project = local.project.project_id + key = "enable-oslogin" + value = "TRUE" + # depend on services or it will fail on destroy + depends_on = [google_project_service.project_services] +} + +resource "google_resource_manager_lien" "lien" { + count = var.lien_reason != "" ? 1 : 0 + parent = "projects/${local.project.number}" + restrictions = ["resourcemanager.projects.delete"] + origin = "created-by-terraform" + reason = var.lien_reason +} + +resource "google_essential_contacts_contact" "contact" { + provider = google-beta + for_each = var.contacts + parent = "projects/${local.project.project_id}" + email = each.key + language_tag = "en" + notification_category_subscriptions = each.value +} + +resource "google_monitoring_monitored_project" "primary" { + provider = google-beta + for_each = toset(var.metric_scopes) + metrics_scope = each.value + name = local.project.project_id +} diff --git a/examples/guardrails/github/project-factory/modules/project/organization-policies.tf b/examples/guardrails/github/project-factory/modules/project/organization-policies.tf new file mode 100644 index 0000000..6870754 --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project/organization-policies.tf @@ -0,0 +1,90 @@ +/** + * 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 + * + * 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 Project-level organization policies. + +resource "google_project_organization_policy" "boolean" { + for_each = var.policy_boolean + project = local.project.project_id + constraint = each.key + + dynamic "boolean_policy" { + for_each = each.value == null ? [] : [each.value] + iterator = policy + content { + enforced = policy.value + } + } + + dynamic "restore_policy" { + for_each = each.value == null ? [""] : [] + content { + default = true + } + } +} + +resource "google_project_organization_policy" "list" { + for_each = var.policy_list + project = local.project.project_id + constraint = each.key + + dynamic "list_policy" { + for_each = each.value.status == null ? [] : [each.value] + iterator = policy + content { + inherit_from_parent = policy.value.inherit_from_parent + suggested_value = policy.value.suggested_value + dynamic "allow" { + for_each = policy.value.status ? [""] : [] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + dynamic "deny" { + for_each = policy.value.status ? [] : [""] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + } + } + + dynamic "restore_policy" { + for_each = each.value.status == null ? [true] : [] + content { + default = true + } + } +} diff --git a/examples/guardrails/github/project-factory/modules/project/outputs.tf b/examples/guardrails/github/project-factory/modules/project/outputs.tf new file mode 100644 index 0000000..10d0e55 --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project/outputs.tf @@ -0,0 +1,87 @@ +/** + * 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 + * + * 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. + */ + +output "custom_roles" { + description = "Ids of the created custom roles." + value = { + for name, role in google_project_iam_custom_role.roles : + name => role.id + } +} + +output "name" { + description = "Project name." + value = local.project.name + depends_on = [ + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "number" { + description = "Project number." + value = local.project.number + depends_on = [ + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "project_id" { + description = "Project id." + value = "${local.prefix}${var.name}" + depends_on = [ + google_project.project, + data.google_project.project, + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "service_accounts" { + description = "Product robot service accounts in project." + value = { + cloud_services = local.service_account_cloud_services + default = local.service_accounts_default + robots = local.service_accounts_robots + } + depends_on = [ + google_project_service.project_services, + google_kms_crypto_key_iam_member.service_identity_cmek, + google_project_service_identity.jit_si, + data.google_bigquery_default_service_account.bq_sa, + data.google_storage_project_service_account.gcs_sa + ] +} + +output "sink_writer_identities" { + description = "Writer identities created for each sink." + value = { + for name, sink in google_logging_project_sink.sink : name => sink.writer_identity + } +} diff --git a/examples/guardrails/github/project-factory/modules/project/service-accounts.tf b/examples/guardrails/github/project-factory/modules/project/service-accounts.tf new file mode 100644 index 0000000..3423524 --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project/service-accounts.tf @@ -0,0 +1,113 @@ +/** + * 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 + * + * 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 Service identities and supporting resources. + +locals { + _service_accounts_cmek_service_dependencies = { + "composer" : [ + "composer", + "artifactregistry", "container-engine", "compute", "pubsub", "storage" + ] + "dataflow" : ["dataflow", "compute"] + } + _service_accounts_robot_services = { + artifactregistry = "service-%s@gcp-sa-artifactregistry" + bq = "bq-%s@bigquery-encryption" + cloudasset = "service-%s@gcp-sa-cloudasset" + cloudbuild = "service-%s@gcp-sa-cloudbuild" + cloudfunctions = "service-%s@gcf-admin-robot" + cloudrun = "service-%s@serverless-robot-prod" + composer = "service-%s@cloudcomposer-accounts" + compute = "service-%s@compute-system" + container-engine = "service-%s@container-engine-robot" + containerregistry = "service-%s@containerregistry" + dataflow = "service-%s@dataflow-service-producer-prod" + dataproc = "service-%s@dataproc-accounts" + gae-flex = "service-%s@gae-api-prod" + # TODO: deprecate gcf + gcf = "service-%s@gcf-admin-robot" + pubsub = "service-%s@gcp-sa-pubsub" + secretmanager = "service-%s@gcp-sa-secretmanager" + storage = "service-%s@gs-project-accounts" + } + service_accounts_default = { + compute = "${local.project.number}-compute@developer.gserviceaccount.com" + gae = "${local.project.project_id}@appspot.gserviceaccount.com" + } + service_account_cloud_services = ( + "${local.project.number}@cloudservices.gserviceaccount.com" + ) + service_accounts_robots = { + for k, v in local._service_accounts_robot_services : + k => "${format(v, local.project.number)}.iam.gserviceaccount.com" + } + service_accounts_jit_services = [ + "secretmanager.googleapis.com", + "pubsub.googleapis.com", + "cloudasset.googleapis.com" + ] + service_accounts_cmek_service_keys = distinct(flatten([ + for s in keys(var.service_encryption_key_ids) : [ + for ss in try(local._service_accounts_cmek_service_dependencies[s], [s]) : [ + for key in var.service_encryption_key_ids[s] : { + service = ss + key = key + } if key != null + ] + ] + ])) +} + +data "google_storage_project_service_account" "gcs_sa" { + count = contains(var.services, "storage.googleapis.com") ? 1 : 0 + project = local.project.project_id + depends_on = [google_project_service.project_services] +} + +data "google_bigquery_default_service_account" "bq_sa" { + count = contains(var.services, "bigquery.googleapis.com") ? 1 : 0 + project = local.project.project_id + depends_on = [google_project_service.project_services] +} + +# Secret Manager SA created just in time, we need to trigger the creation. +resource "google_project_service_identity" "jit_si" { + for_each = setintersection(var.services, local.service_accounts_jit_services) + provider = google-beta + project = local.project.project_id + service = each.value + depends_on = [google_project_service.project_services] +} + +resource "google_kms_crypto_key_iam_member" "service_identity_cmek" { + for_each = { + for service_key in local.service_accounts_cmek_service_keys : + "${service_key.service}.${service_key.key}" => service_key + if service_key != service_key.key + } + crypto_key_id = each.value.key + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + member = "serviceAccount:${local.service_accounts_robots[each.value.service]}" + depends_on = [ + google_project.project, + google_project_service.project_services, + google_project_service_identity.jit_si, + data.google_bigquery_default_service_account.bq_sa, + data.google_project.project, + data.google_storage_project_service_account.gcs_sa, + ] +} diff --git a/examples/guardrails/github/project-factory/modules/project/shared-vpc.tf b/examples/guardrails/github/project-factory/modules/project/shared-vpc.tf new file mode 100644 index 0000000..9c7bd71 --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project/shared-vpc.tf @@ -0,0 +1,75 @@ +/** + * 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 + * + * 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 Shared VPC project-level configuration. + +locals { + # compute the host project IAM bindings for this project's service identities + _svpc_service_identity_iam = coalesce( + local.svpc_service_config.service_identity_iam, {} + ) + _svpc_service_iam = flatten([ + for role, services in local._svpc_service_identity_iam : [ + for service in services : { role = role, service = service } + ] + ]) + svpc_host_config = { + enabled = coalesce( + try(var.shared_vpc_host_config.enabled, null), false + ) + service_projects = coalesce( + try(var.shared_vpc_host_config.service_projects, null), [] + ) + } + svpc_service_config = coalesce(var.shared_vpc_service_config, { + host_project = null, service_identity_iam = {} + }) + svpc_service_iam = { + for b in local._svpc_service_iam : "${b.role}:${b.service}" => b + } +} + +resource "google_compute_shared_vpc_host_project" "shared_vpc_host" { + provider = google-beta + count = local.svpc_host_config.enabled ? 1 : 0 + project = local.project.project_id +} + +resource "google_compute_shared_vpc_service_project" "service_projects" { + provider = google-beta + for_each = toset(local.svpc_host_config.service_projects) + host_project = local.project.project_id + service_project = each.value + depends_on = [google_compute_shared_vpc_host_project.shared_vpc_host] +} + +resource "google_compute_shared_vpc_service_project" "shared_vpc_service" { + provider = google-beta + count = local.svpc_service_config.host_project != null ? 1 : 0 + host_project = var.shared_vpc_service_config.host_project + service_project = local.project.project_id +} + +resource "google_project_iam_member" "shared_vpc_host_robots" { + for_each = local.svpc_service_iam + project = var.shared_vpc_service_config.host_project + role = each.value.role + member = ( + each.value.service == "cloudservices" + ? "serviceAccount:${local.service_account_cloud_services}" + : "serviceAccount:${local.service_accounts_robots[each.value.service]}" + ) +} diff --git a/examples/guardrails/github/project-factory/modules/project/tags.tf b/examples/guardrails/github/project-factory/modules/project/tags.tf new file mode 100644 index 0000000..683143b --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project/tags.tf @@ -0,0 +1,21 @@ +/** + * 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 + * + * 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. + */ + +resource "google_tags_tag_binding" "binding" { + for_each = coalesce(var.tag_bindings, {}) + parent = "//cloudresourcemanager.googleapis.com/projects/${local.project.number}" + tag_value = each.value +} diff --git a/examples/guardrails/github/project-factory/modules/project/variables.tf b/examples/guardrails/github/project-factory/modules/project/variables.tf new file mode 100644 index 0000000..578f9d2 --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project/variables.tf @@ -0,0 +1,259 @@ +/** + * 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 + * + * 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. + */ + +variable "auto_create_network" { + description = "Whether to create the default network for the project." + type = bool + default = false +} + +variable "billing_account" { + description = "Billing account id." + type = string + default = null +} + +variable "contacts" { + description = "List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES." + type = map(list(string)) + default = {} + nullable = false +} + +variable "custom_roles" { + description = "Map of role name => list of permissions to create in this project." + type = map(list(string)) + default = {} + nullable = false +} + +variable "descriptive_name" { + description = "Name of the project name. Used for project name instead of `name` variable." + type = string + default = null +} + +variable "group_iam" { + description = "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." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam" { + description = "IAM bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_additive" { + description = "IAM additive bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +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 "labels" { + description = "Resource labels." + type = map(string) + default = {} + nullable = false +} + +variable "lien_reason" { + description = "If non-empty, creates a project lien with this description." + type = string + default = "" +} + +variable "logging_exclusions" { + description = "Logging exclusions for this project in the form {NAME -> FILTER}." + type = map(string) + default = {} + nullable = false +} + +variable "logging_sinks" { + description = "Logging sinks to create for this project." + type = map(object({ + destination = string + type = string + filter = string + iam = bool + unique_writer = bool + # TODO exclusions also support description and disabled + exclusions = map(string) + })) + validation { + condition = alltrue([ + for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } + default = {} + nullable = false +} + +variable "metric_scopes" { + description = "List of projects that will act as metric scopes for this project." + type = list(string) + default = [] + nullable = false +} + +variable "name" { + description = "Project name and id suffix." + type = string +} + +variable "oslogin" { + description = "Enable OS Login." + type = bool + default = false +} + +variable "oslogin_admins" { + description = "List of IAM-style identities that will be granted roles necessary for OS Login administrators." + type = list(string) + default = [] + nullable = false + +} + +variable "oslogin_users" { + description = "List of IAM-style identities that will be granted roles necessary for OS Login users." + type = list(string) + default = [] + nullable = false +} + +variable "parent" { + description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "policy_boolean" { + description = "Map of boolean org policies and enforcement value, set value to null for policy restore." + type = map(bool) + default = {} + nullable = false +} + +variable "policy_list" { + description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." + type = map(object({ + inherit_from_parent = bool + suggested_value = string + status = bool + values = list(string) + })) + default = {} + nullable = false +} + +variable "prefix" { + description = "Prefix used to generate project id and name." + type = string + default = null +} + +variable "project_create" { + description = "Create project. When set to false, uses a data source to reference existing project." + type = bool + default = true +} + +variable "service_config" { + description = "Configure service API activation." + type = object({ + disable_on_destroy = bool + disable_dependent_services = bool + }) + default = { + disable_on_destroy = true + disable_dependent_services = true + } +} + +variable "service_encryption_key_ids" { + description = "Cloud KMS encryption key in {SERVICE => [KEY_URL]} format." + type = map(list(string)) + default = {} +} + +# accessPolicies/ACCESS_POLICY_NAME/servicePerimeters/PERIMETER_NAME +variable "service_perimeter_bridges" { + description = "Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format." + type = list(string) + default = null +} + +# accessPolicies/ACCESS_POLICY_NAME/servicePerimeters/PERIMETER_NAME +variable "service_perimeter_standard" { + description = "Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format." + type = string + default = null +} + +variable "services" { + description = "Service APIs to enable." + type = list(string) + default = [] +} + +variable "shared_vpc_host_config" { + description = "Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project)." + type = object({ + enabled = bool + service_projects = list(string) + }) + default = null +} + +variable "shared_vpc_service_config" { + description = "Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config)." + # the list of valid service identities is in service-accounts.tf + type = object({ + host_project = string + service_identity_iam = map(list(string)) + }) + default = null +} + +variable "skip_delete" { + description = "Allows the underlying resources to be destroyed without destroying the project itself." + type = bool + default = false +} + +variable "tag_bindings" { + description = "Tag bindings for this project, in key => tag value id format." + type = map(string) + default = null +} diff --git a/examples/guardrails/github/project-factory/modules/project/versions.tf b/examples/guardrails/github/project-factory/modules/project/versions.tf new file mode 100644 index 0000000..e72a780 --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.1.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/github/project-factory/modules/project/vpc-sc.tf b/examples/guardrails/github/project-factory/modules/project/vpc-sc.tf new file mode 100644 index 0000000..edaa203 --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project/vpc-sc.tf @@ -0,0 +1,45 @@ +/** + * 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 + * + * 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 VPC-SC project-level perimeter configuration. + +moved { + from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-standard + to = google_access_context_manager_service_perimeter_resource.standard +} + +resource "google_access_context_manager_service_perimeter_resource" "standard" { + count = var.service_perimeter_standard != null ? 1 : 0 + # this needs an additional lifecycle block in the vpc module on the + # google_access_context_manager_service_perimeter resource + perimeter_name = var.service_perimeter_standard + resource = "projects/${local.project.number}" +} + +moved { + from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-bridges + to = google_access_context_manager_service_perimeter_resource.bridge +} + +resource "google_access_context_manager_service_perimeter_resource" "bridge" { + for_each = toset( + var.service_perimeter_bridges != null ? var.service_perimeter_bridges : [] + ) + # this needs an additional lifecycle block in the vpc module on the + # google_access_context_manager_service_perimeter resource + perimeter_name = each.value + resource = "projects/${local.project.number}" +} diff --git a/examples/guardrails/github/project-factory/modules/project_plus/README.md b/examples/guardrails/github/project-factory/modules/project_plus/README.md new file mode 100644 index 0000000..2976a30 --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project_plus/README.md @@ -0,0 +1 @@ +This is an addon for the project module with connects one service account to one project. \ No newline at end of file diff --git a/examples/guardrails/github/project-factory/modules/project_plus/main.tf b/examples/guardrails/github/project-factory/modules/project_plus/main.tf new file mode 100644 index 0000000..7330dd0 --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project_plus/main.tf @@ -0,0 +1,46 @@ +/** + * 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 + * + * 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. + */ + +resource "random_id" "rand" { + byte_length = 4 +} + +module "project" { + source = "./../project" + name = "${var.team}-prj-${random_id.rand.hex}" + parent = var.folder + billing_account = var.billing_account +} + +resource "google_service_account" "sa" { + account_id = "${var.team}-sa-${random_id.rand.hex}" + display_name = "Service account ${var.team}" + project = module.project.project_id +} + +resource "google_service_account_iam_member" "sa-iam" { + service_account_id = google_service_account.sa.name + role = "roles/iam.workloadIdentityUser" + member = "principalSet://iam.googleapis.com/${var.wif-pool}/attribute.sub/${var.repo_sub}" +} + +resource "google_project_iam_member" "sa-project" { + for_each = toset(var.roles) + role = each.value + member = "serviceAccount:${google_service_account.sa.email}" + project = module.project.project_id + depends_on = [google_service_account.sa] +} diff --git a/examples/guardrails/github/project-factory/modules/project_plus/outputs.tf b/examples/guardrails/github/project-factory/modules/project_plus/outputs.tf new file mode 100644 index 0000000..c589d87 --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project_plus/outputs.tf @@ -0,0 +1,36 @@ +/** + * 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 + * + * 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. + */ + +output "project_id" { + description = "Project ID" + value = module.project.project_id +} + +output "service_account_email" { + description = "Service Account Email" + value = google_service_account.sa.email +} + +output "repo_sub" { + description = "Repository" + value = var.repo_sub +} + +output "repo_provider" { + description = "Repository Provider" + value = var.repo_provider +} + diff --git a/examples/guardrails/github/project-factory/modules/project_plus/variables.tf b/examples/guardrails/github/project-factory/modules/project_plus/variables.tf new file mode 100644 index 0000000..67524ae --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project_plus/variables.tf @@ -0,0 +1,52 @@ +/** + * 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 + * + * 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. + */ + + +variable "team" { + description = "Team name." + type = string +} + +variable "repo_sub" { + description = "Repository path" + type = string +} + +variable "repo_provider" { + description = "Repository provider" + type = string +} + +variable "billing_account" { + description = "Billing account name." + type = string +} + +variable "folder" { + description = "Folder name." + type = string +} + +variable "roles" { + description = "Roles to attach." + type = list(string) + default = [] +} + +variable "wif-pool" { + description = "WIF pool name." + type = string +} diff --git a/examples/guardrails/github/project-factory/modules/project_plus/versions.tf b/examples/guardrails/github/project-factory/modules/project_plus/versions.tf new file mode 100644 index 0000000..2904126 --- /dev/null +++ b/examples/guardrails/github/project-factory/modules/project_plus/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.0.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/github/project-factory/outputs.tf b/examples/guardrails/github/project-factory/outputs.tf new file mode 100644 index 0000000..43d9c4f --- /dev/null +++ b/examples/guardrails/github/project-factory/outputs.tf @@ -0,0 +1,40 @@ +/** + * 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 + * + * 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. + */ + +output "wif_pool_id_gitlab" { + description = "Gitlab Workload Identity Pool" + value = google_iam_workload_identity_pool.wif-pool-gitlab.name +} + +output "wif_provider_id_gitlab" { + description = "Gitlab Workload Identity Pool" + value = google_iam_workload_identity_pool_provider.wif-provider-gitlab.name +} + +output "wif_pool_id_github" { + description = "Github Workload Identity Pool" + value = google_iam_workload_identity_pool.wif-pool-github.name +} + +output "wif_provider_id_github" { + description = "Github Workload Identity Pool" + value = google_iam_workload_identity_pool_provider.wif-provider-github.name +} + +output "projects" { + description = "Created projects and service accounts." + value = module.project +} \ No newline at end of file diff --git a/examples/guardrails/github/project-factory/provider.tf b/examples/guardrails/github/project-factory/provider.tf new file mode 100644 index 0000000..34beb2f --- /dev/null +++ b/examples/guardrails/github/project-factory/provider.tf @@ -0,0 +1,28 @@ +/** + * 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 + * + * 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. + */ + +terraform { + backend "gcs" { + } +} + +provider "google" { + +} + +provider "google-beta" { + +} diff --git a/examples/guardrails/github/project-factory/variables.tf b/examples/guardrails/github/project-factory/variables.tf new file mode 100644 index 0000000..df6e79d --- /dev/null +++ b/examples/guardrails/github/project-factory/variables.tf @@ -0,0 +1,19 @@ +/** + * 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 + * + * 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. + */ + +variable "folder" { + +} diff --git a/examples/guardrails/github/project-factory/wif.tf b/examples/guardrails/github/project-factory/wif.tf new file mode 100644 index 0000000..c54c67f --- /dev/null +++ b/examples/guardrails/github/project-factory/wif.tf @@ -0,0 +1,67 @@ +/** + * 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 + * + * 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. + */ + +resource "random_id" "rand" { + byte_length = 4 +} + +module "wif-project" { + source = "./modules/project" + name = "wif-prj-${random_id.rand.hex}" + parent = var.folder + billing_account = "01B3B2-962224-4EEC67" +} + +resource "google_iam_workload_identity_pool" "wif-pool-gitlab" { + provider = google-beta + workload_identity_pool_id = "gitlab-pool-${random_id.rand.hex}" + project = module.wif-project.project_id +} + +resource "google_iam_workload_identity_pool_provider" "wif-provider-gitlab" { + provider = google-beta + workload_identity_pool_id = google_iam_workload_identity_pool.wif-pool-gitlab.workload_identity_pool_id + workload_identity_pool_provider_id = "gitlab-provider-${random_id.rand.hex}" + project = module.wif-project.project_id + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.sub" = "assertion.sub" + } + oidc { + issuer_uri = "https://gitlab.com" + } +} + +resource "google_iam_workload_identity_pool" "wif-pool-github" { + provider = google-beta + workload_identity_pool_id = "github-pool-${random_id.rand.hex}" + project = module.wif-project.project_id +} + +resource "google_iam_workload_identity_pool_provider" "wif-provider-github" { + provider = google-beta + workload_identity_pool_id = google_iam_workload_identity_pool.wif-pool-github.workload_identity_pool_id + workload_identity_pool_provider_id = "github-provider-${random_id.rand.hex}" + project = module.wif-project.project_id + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.sub" = "assertion.sub" + "attribute.actor" = "assertion.actor" + } + oidc { + issuer_uri = "https://token.actions.githubusercontent.com" + } +} diff --git a/examples/guardrails/github/skunkworks/.github/workflows/tf-actions-dev.yml b/examples/guardrails/github/skunkworks/.github/workflows/tf-actions-dev.yml new file mode 100644 index 0000000..2fbf4c1 --- /dev/null +++ b/examples/guardrails/github/skunkworks/.github/workflows/tf-actions-dev.yml @@ -0,0 +1,91 @@ +# 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 +# +# 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. + +name: 'DEV Deployment' + +on: + push: + branches: + - dev + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.DEV_SERVICE_ACCOUNT }} + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve + + diff --git a/examples/guardrails/github/skunkworks/.github/workflows/tf-actions-prod.yml b/examples/guardrails/github/skunkworks/.github/workflows/tf-actions-prod.yml new file mode 100644 index 0000000..4159feb --- /dev/null +++ b/examples/guardrails/github/skunkworks/.github/workflows/tf-actions-prod.yml @@ -0,0 +1,91 @@ +# 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 +# +# 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. + +name: 'PROD Deployment' + +on: + push: + branches: + - main + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.PROD_SERVICE_ACCOUNT }} + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve + + diff --git a/examples/guardrails/github/skunkworks/.github/workflows/tf-actions-stage.yml b/examples/guardrails/github/skunkworks/.github/workflows/tf-actions-stage.yml new file mode 100644 index 0000000..0f20fa0 --- /dev/null +++ b/examples/guardrails/github/skunkworks/.github/workflows/tf-actions-stage.yml @@ -0,0 +1,91 @@ +# 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 +# +# 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. + +name: 'STAGE Deployment' + +on: + push: + branches: + - stage + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.STAGE_SERVICE_ACCOUNT }} + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve + + diff --git a/examples/guardrails/github/skunkworks/README.md b/examples/guardrails/github/skunkworks/README.md new file mode 100644 index 0000000..0b89bb2 --- /dev/null +++ b/examples/guardrails/github/skunkworks/README.md @@ -0,0 +1,29 @@ +# Skunkworks - IaC Kickstarter Template + +This is a template for an IaC kickstarter repository. + +![Skunkworks](https://user-images.githubusercontent.com/94000358/169810982-36f01de2-e5e5-4ecd-b98e-3cf5a6aa9f81.png) + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +This template creates a bucket in the specified target environment. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: + +Secret configuration + +or by modifing the [Workflow Action Files](.github/workflows/) and setting the environment variables: +``` +env: + STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' + # The workload identity provider that should be used for this repository. + SERVICE_ACCOUNT: 'XXXX@XXXX' + # The service account that should be used for this repository. +``` diff --git a/examples/guardrails/github/skunkworks/main.tf b/examples/guardrails/github/skunkworks/main.tf new file mode 100644 index 0000000..03926bd --- /dev/null +++ b/examples/guardrails/github/skunkworks/main.tf @@ -0,0 +1,23 @@ +/** + * 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 + * + * 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. + */ + +resource "google_storage_bucket" "bucket" { + project = var.project + name = lower("${var.project}-test-bucket") + location = "EU" + force_destroy = true +} + diff --git a/examples/guardrails/github/skunkworks/provider.tf b/examples/guardrails/github/skunkworks/provider.tf new file mode 100644 index 0000000..0802a09 --- /dev/null +++ b/examples/guardrails/github/skunkworks/provider.tf @@ -0,0 +1,28 @@ +/** + * 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 + * + * 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. + */ + +terraform { + backend "gcs" { + } +} + +provider "google" { + +} + +provider "google-beta" { + +} \ No newline at end of file diff --git a/examples/guardrails/github/skunkworks/variables.tf b/examples/guardrails/github/skunkworks/variables.tf new file mode 100644 index 0000000..6af98ff --- /dev/null +++ b/examples/guardrails/github/skunkworks/variables.tf @@ -0,0 +1,20 @@ +/** + * 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 + * + * 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. + */ + +variable "project" { + type = string + default = "project-id" +} diff --git a/examples/guardrails/gitlab/folder-factory/.gitignore b/examples/guardrails/gitlab/folder-factory/.gitignore new file mode 100644 index 0000000..a5d8c0e --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.terraform +.terraform.lock.hcl diff --git a/examples/guardrails/gitlab/folder-factory/.gitlab/workflows/.gitlab-ci.yml b/examples/guardrails/gitlab/folder-factory/.gitlab/workflows/.gitlab-ci.yml new file mode 100644 index 0000000..dceaa74 --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/.gitlab/workflows/.gitlab-ci.yml @@ -0,0 +1,134 @@ +#The pipeline only gets triggered only when a change is committed to one of the following directories mentioned below. +#To update this behavior, add or remove items under changes: section. +workflow: + rules: + - changes: + - "*.tf" + - "*.tfvars" + - "data/**/*" + - "modules/**/*" + +# Workflow image +default: + image: + name: google/cloud-sdk:slim + # pull_policy: if-not-present #Not allowed on the SaaS gitlab runners. + +# Workflow variables. They can be overwritten by passing pipeline Variables in Gitlab repository +variables: + TF_VERSION: $TF_VERSION + TF_ROOT: $TF_ROOT + TF_LOG: $TF_LOG + TF_PLAN_NAME: plan.tfplan + TF_PLAN_JSON: plan.json + REFRESH: -refresh=true + STATE_BUCKET: $STATE_BUCKET + GCP_PROJECT_ID: $GCP_PROJECT_ID + GCP_WORKLOAD_IDENTITY_PROVIDER: $GCP_WORKLOAD_IDENTITY_PROVIDER + GCP_SERVICE_ACCOUNT: $GCP_SERVICE_ACCOUNT + +# Provides a list of stages for this GitLab workflow +stages: + - setup-terraform + - validate + - plan + - apply + +.gcp-auth: &gcp-auth + - echo ${CI_JOB_JWT_V2} > .ci_job_jwt_file + - gcloud iam workload-identity-pools create-cred-config ${GCP_WORKLOAD_IDENTITY_PROVIDER} --service-account="${GCP_SERVICE_ACCOUNT}" --output-file=.gcp_temp_cred.json --credential-source-file=.ci_job_jwt_file + - gcloud auth login --cred-file=`pwd`/.gcp_temp_cred.json + - gcloud config set project $GCP_PROJECT_ID + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/.gcp_temp_cred.json + +.terraform-ver-init: &terraform-ver-init + - cd $TF_ROOT + - cp ./terraform /usr/bin/ + - terraform init -backend-config="bucket=$STATE_BUCKET" -backend-config="prefix=$CI_PROJECT_NAME" --upgrade=True + +# Cache files between jobs +cache: + key: "$CI_COMMIT_SHA" + # Globally caches the .terraform folder across each job in this workflow + paths: + - $TF_ROOT/.terraform + +#Job: setup-terraform | Stage: setup-terraform +# Purpose: downloads specified version of terraform binary and passes it as artifact for the other jobs and stages +setup-terraform: + stage: setup-terraform + script: + - /bin/sh -c 'apt-get update && apt -y install unzip wget && wget https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip && unzip terraform_${TF_VERSION}_linux_amd64.zip' + artifacts: + untracked: false + paths: + - terraform + +#Job: tf-fmt | Stage: validate +# Purpose: check the format (fmt) as a sort of linting test +tf-fmt: + stage: validate + dependencies: + - setup-terraform + before_script: + - *gcp-auth + - *terraform-ver-init + script: + - terraform fmt -recursive -check + allow_failure: false + +# Job: Validate | Stage: Validate +# Purpose: Syntax Validation for the Terraform configuration files +validate: + stage: validate + dependencies: + - setup-terraform + before_script: + - *gcp-auth + - *terraform-ver-init + script: + - terraform validate + allow_failure: false + +#Job: plan | Stage: Plan +#Runs terraform plan and outputs the plan and a json summary to +#local files which are later made available as artifacts. +plan: + stage: plan + dependencies: + - setup-terraform + - validate + before_script: + - *gcp-auth + - *terraform-ver-init + - apt install -y jq + - shopt -s expand_aliases && alias convert_report="jq -r '([.resource_changes[]?.change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'" + script: + - cd $TF_ROOT + - terraform plan -out=$TF_PLAN_NAME $REFRESH + - terraform show --json $TF_PLAN_NAME | convert_report > $TF_PLAN_JSON + allow_failure: false + + artifacts: + reports: + terraform: ${TF_ROOT}/$TF_PLAN_JSON + paths: + - ${TF_ROOT}/$TF_PLAN_NAME + - ${TF_ROOT}/$TF_PLAN_JSON + expire_in: 7 days #optional. Gitlab stores artifacts of successful pipelines for the most recent commit on each ref. If needed, enable "Keep artifacts from most recent successful jobs" in CI/CD settings of the repository. + +#Stage:apply | job: apply +# purpose: executes the plan from the file created in the plan stage +apply: + stage: apply + before_script: + - *gcp-auth + - *terraform-ver-init + dependencies: + - setup-terraform + - plan + script: + - cd $TF_ROOT + - terraform apply -auto-approve $TF_PLAN_NAME + when: manual #Set as manual currently as WIF doesn't support merge request pipelines for now. + allow_failure: false \ No newline at end of file diff --git a/examples/guardrails/gitlab/folder-factory/.gitlab/workflows/README.md b/examples/guardrails/gitlab/folder-factory/.gitlab/workflows/README.md new file mode 100644 index 0000000..6f4b19f --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/.gitlab/workflows/README.md @@ -0,0 +1,45 @@ + +# Terraform pipeline execution with Gitlab runners and Gitlab repository + +This workflow runs terraform pipelines using Gitlab CICD. This document covers the steps to setup Gitlab CICD and provides a high level overview of the stages involved: + +## Prerequisites + +* A Gitlab project containing the folder-factory repository +* Available Gitlab Runners for the project either self hosted or the SaaS Gitlab shared runners. +* A working WIF provider, pool setup with audience as gitlab.com with neccessary attributes and linked service account with required permissions. For further details, please refer to the official [Gitlab Documentation](https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/) + + +## Setup + +1. Update the CICD configuration file path in the repository + * From the folder-factory Gitlab project page, Navigate to Settings > CICD > expand General pipelines + * update CI/CD configuration file value to the relative path of the gitlab-ci.yml file from the root directory + +2. Update the CI/CD variables + * From the folder-factory project page, Navigate to Settings > CICD > expand Variables + * Add the below variables to the pipeline + +| Variable | Description | Sample value | +|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| GCP_PROJECT_ID | The GCP project ID of your service account | sample-project-1122 | +| GCP_SERVICE_ACCOUNT | The Service Account to be used for creating folders | xyz@sample-project-1122.iam.gserviceaccount.com | +| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects//locations/global/workloadIdentityPools//providers/ | +| STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | +| TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | +| TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | +| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | + +## Overview of the Pipeline stages +The complete workflow consists of 4 stages and 2 before-script jobs + +* before_script jobs : + * gcp-auth : creates the wif credentials by impersonating the service account. + * terraform init : initializes terraform in the specified TF_ROOT directory + +* Stages: + * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages + * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command + * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts + * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once plan is successful. Runs terraform apply and creates the infrastructure specified. + diff --git a/examples/guardrails/gitlab/folder-factory/README.md b/examples/guardrails/gitlab/folder-factory/README.md new file mode 100644 index 0000000..a139948 --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/README.md @@ -0,0 +1,59 @@ +# Folder Factory + +This is a template for a DevOps folder factory. + +It can be used with [https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory](https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory) and is intended to house the folder configurations: + +![Screenshot 2022-05-10 12 00 19 PM](https://user-images.githubusercontent.com/94000358/169809437-aaa8538e-3ffc-48b3-9028-84e4995de150.png) + +Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: + +Secret configuration + +or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: +``` +env: + STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' + # The workload identity provider that should be used for this repository. + SERVICE_ACCOUNT: 'XXXX@XXXX' + # The service account that should be used for this repository. +``` + +## Setting up folders + +The folder factory will: +- create a folders with defined organisational policies + +It uses YAML configuration files for every folder with the following sample structure: +``` +parent: folders/XXXXXXXXX +org_policies: + policy_boolean: + constraints/compute.disableGuestAttributesAccess: true + constraints/iam.disableServiceAccountCreation: false + constraints/iam.disableServiceAccountKeyCreation: false + constraints/iam.disableServiceAccountKeyUpload: false + constraints/gcp.disableCloudLogging: false + policy_list: + constraints/compute.vmExternalIpAccess: + inherit_from_parent: null + status: true + suggested_value: null + values: +iam: + roles/resourcemanager.projectCreator: + - serviceAccount:XXXXX@XXXXXX +``` + +Every folder is defined with its own yaml file located in the following [Folder](data/folder). diff --git a/examples/guardrails/gitlab/folder-factory/data/folders/folder.yaml.sample b/examples/guardrails/gitlab/folder-factory/data/folders/folder.yaml.sample new file mode 100644 index 0000000..9ea745d --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/data/folders/folder.yaml.sample @@ -0,0 +1,31 @@ +# 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 +# +# 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. + +parent: folders/01234567890 +org_policies: + policy_boolean: + constraints/compute.disableGuestAttributesAccess: true + constraints/iam.disableServiceAccountCreation: false + constraints/iam.disableServiceAccountKeyCreation: false + constraints/iam.disableServiceAccountKeyUpload: false + constraints/gcp.disableCloudLogging: false + policy_list: + constraints/compute.vmExternalIpAccess: + inherit_from_parent: null + status: true + suggested_value: null + values: +iam: + roles/resourcemanager.projectCreator: + - serviceAccount:service-account@project-xyz.iam.gserviceaccount.com \ No newline at end of file diff --git a/examples/guardrails/gitlab/folder-factory/main.tf b/examples/guardrails/gitlab/folder-factory/main.tf new file mode 100644 index 0000000..d024c17 --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/main.tf @@ -0,0 +1,32 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + folders = { + for f in fileset("./data/folders", "**/*.yaml") : + trimsuffix(f, ".yaml") => yamldecode(file("./data/folders/${f}")) + } +} + +module "folder" { + source = "./modules/folder" + for_each = local.folders + name = each.key + parent = each.value.parent + policy_boolean = try(each.value.org_policies.policy_boolean, {}) + policy_list = try(each.value.org_policies.policy_list, {}) + iam = try(each.value.iam, {}) +} \ No newline at end of file diff --git a/examples/guardrails/gitlab/folder-factory/modules/folder/README.md b/examples/guardrails/gitlab/folder-factory/modules/folder/README.md new file mode 100644 index 0000000..4f6898b --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/modules/folder/README.md @@ -0,0 +1,299 @@ +# Google Cloud Folder Module + +This module allows the creation and management of folders, including support for IAM bindings, organization policies, and hierarchical firewall rules. + +## Examples + +### IAM bindings + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + group_iam = { + "cloud-owners@example.org" = [ + "roles/owner", + "roles/resourcemanager.projectCreator" + ] + } + iam = { + "roles/owner" = ["user:one@example.com"] + } +} +# tftest modules=1 resources=3 +``` + +### Organization policies + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + policy_boolean = { + "constraints/compute.disableGuestAttributesAccess" = true + "constraints/compute.skipDefaultNetworkCreation" = true + } + policy_list = { + "constraints/compute.trustedImageProjects" = { + inherit_from_parent = null + suggested_value = null + status = true + values = ["projects/my-project"] + } + } +} +# tftest modules=1 resources=4 +``` + +### Firewall policy factory + +In the same way as for the [organization](../organization) module, the in-built factory allows you to define a single policy, using one file for rules, and an optional file for CIDR range substitution variables. Remember that non-absolute paths are relative to the root module (the folder where you run `terraform`). + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + firewall_policy_factory = { + cidr_file = "data/cidrs.yaml" + policy_name = null + rules_file = "data/rules.yaml" + } + firewall_policy_association = { + factory-policy = module.folder.firewall_policy_id["factory"] + } +} +# tftest skip +``` + +```yaml +# cidrs.yaml + +rfc1918: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 +``` + +```yaml +# rules.yaml + +allow-admins: + description: Access from the admin subnet to all subnets + direction: INGRESS + action: allow + priority: 1000 + ranges: + - $rfc1918 + ports: + all: [] + target_resources: null + enable_logging: false + +allow-ssh-from-iap: + description: Enable SSH from IAP + direction: INGRESS + action: allow + priority: 1002 + ranges: + - 35.235.240.0/20 + ports: + tcp: ["22"] + target_resources: null + enable_logging: false +``` + +### Logging Sinks + +```hcl +module "gcs" { + source = "./modules/gcs" + project_id = "my-project" + name = "gcs_sink" + force_destroy = true +} + +module "dataset" { + source = "./modules/bigquery-dataset" + project_id = "my-project" + id = "bq_sink" +} + +module "pubsub" { + source = "./modules/pubsub" + project_id = "my-project" + name = "pubsub_sink" +} + +module "bucket" { + source = "./modules/logging-bucket" + parent_type = "project" + parent = "my-project" + id = "bucket" +} + +module "folder-sink" { + source = "./modules/folder" + parent = "folders/657104291943" + name = "my-folder" + logging_sinks = { + warnings = { + type = "storage" + destination = module.gcs.name + filter = "severity=WARNING" + include_children = true + exclusions = {} + } + info = { + type = "bigquery" + destination = module.dataset.id + filter = "severity=INFO" + include_children = true + exclusions = {} + } + notice = { + type = "pubsub" + destination = module.pubsub.id + filter = "severity=NOTICE" + include_children = true + exclusions = {} + } + debug = { + type = "logging" + destination = module.bucket.id + filter = "severity=DEBUG" + include_children = true + exclusions = { + no-compute = "logName:compute" + } + } + } + logging_exclusions = { + no-gce-instances = "resource.type=gce_instance" + } +} +# tftest modules=5 resources=14 +``` + +### Hierarchical firewall policies + +```hcl +module "folder1" { + source = "./modules/folder" + parent = var.organization_id + name = "policy-container" + + firewall_policies = { + iap-policy = { + allow-iap-ssh = { + description = "Always allow ssh from IAP" + direction = "INGRESS" + action = "allow" + priority = 100 + ranges = ["35.235.240.0/20"] + ports = { tcp = ["22"] } + target_service_accounts = null + target_resources = null + logging = false + } + } + } + firewall_policy_association = { + iap-policy = "iap-policy" + } +} + +module "folder2" { + source = "./modules/folder" + parent = var.organization_id + name = "hf2" + firewall_policy_association = { + iap-policy = module.folder1.firewall_policy_id["iap-policy"] + } +} +# tftest modules=2 resources=6 +``` + +## 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. + +```hcl +module "org" { + source = "./modules/organization" + organization_id = var.organization_id + tags = { + environment = { + description = "Environment specification." + iam = null + values = { + dev = null + prod = null + } + } + } +} + +module "folder" { + source = "./modules/folder" + name = "Test" + parent = module.org.organization_id + tag_bindings = { + env-prod = module.org.tag_values["environment/prod"].id + foo = "tagValues/12345678" + } +} +# tftest modules=2 resources=6 +``` + + + + +## Files + +| name | description | resources | +|---|---|---| +| [firewall-policies.tf](./firewall-policies.tf) | None | google_compute_firewall_policy · google_compute_firewall_policy_association · google_compute_firewall_policy_rule | +| [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_folder_iam_binding | +| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_folder_exclusion · google_logging_folder_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | +| [main.tf](./main.tf) | Module-level locals and resources. | google_essential_contacts_contact · google_folder | +| [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_folder_organization_policy | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding | +| [variables.tf](./variables.tf) | Module variables. | | +| [versions.tf](./versions.tf) | Version pins. | | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | +| [firewall_policies](variables.tf#L24) | Hierarchical firewall policies created in this folder. | map(map(object({…}))) | | {} | +| [firewall_policy_association](variables.tf#L41) | The hierarchical firewall policy to associate to this folder. Must be either a key in the `firewall_policies` map or the id of a policy defined somewhere else. | map(string) | | {} | +| [firewall_policy_factory](variables.tf#L48) | Configuration for the firewall policy factory. | object({…}) | | null | +| [folder_create](variables.tf#L58) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | +| [group_iam](variables.tf#L64) | 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. | map(list(string)) | | {} | +| [iam](variables.tf#L71) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [id](variables.tf#L78) | Folder ID in case you use folder_create=false. | string | | null | +| [logging_exclusions](variables.tf#L84) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L91) | Logging sinks to create for this folder. | map(object({…})) | | {} | +| [name](variables.tf#L112) | Folder name. | string | | null | +| [parent](variables.tf#L118) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [policy_boolean](variables.tf#L128) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | +| [policy_list](variables.tf#L135) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L147) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [firewall_policies](outputs.tf#L16) | Map of firewall policy resources created in this folder. | | +| [firewall_policy_id](outputs.tf#L21) | Map of firewall policy ids created in this folder. | | +| [folder](outputs.tf#L26) | Folder resource. | | +| [id](outputs.tf#L31) | Folder id. | | +| [name](outputs.tf#L41) | Folder name. | | +| [sink_writer_identities](outputs.tf#L46) | Writer identities created for each sink. | | + + diff --git a/examples/guardrails/gitlab/folder-factory/modules/folder/firewall-policies.tf b/examples/guardrails/gitlab/folder-factory/modules/folder/firewall-policies.tf new file mode 100644 index 0000000..96224c5 --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/modules/folder/firewall-policies.tf @@ -0,0 +1,93 @@ +/** + * 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 + * + * 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 { + _factory_cidrs = try( + yamldecode(file(var.firewall_policy_factory.cidr_file)), {} + ) + _factory_name = ( + try(var.firewall_policy_factory.policy_name, null) == null + ? "factory" + : var.firewall_policy_factory.policy_name + ) + _factory_rules = try( + yamldecode(file(var.firewall_policy_factory.rules_file)), {} + ) + _factory_rules_parsed = { + for name, rule in local._factory_rules : name => merge(rule, { + ranges = flatten([ + for r in(rule.ranges == null ? [] : rule.ranges) : + lookup(local._factory_cidrs, trimprefix(r, "$"), r) + ]) + }) + } + _merged_rules = flatten([ + for policy, rules in local.firewall_policies : [ + for name, rule in rules : merge(rule, { + policy = policy + name = name + }) + ] + ]) + firewall_policies = merge(var.firewall_policies, ( + length(local._factory_rules) == 0 + ? {} + : { (local._factory_name) = local._factory_rules_parsed } + )) + firewall_rules = { + for r in local._merged_rules : "${r.policy}-${r.name}" => r + } +} + +resource "google_compute_firewall_policy" "policy" { + for_each = local.firewall_policies + short_name = each.key + parent = local.folder.id +} + +resource "google_compute_firewall_policy_rule" "rule" { + for_each = local.firewall_rules + firewall_policy = google_compute_firewall_policy.policy[each.value.policy].id + action = each.value.action + direction = each.value.direction + priority = try(each.value.priority, null) + target_resources = try(each.value.target_resources, null) + target_service_accounts = try(each.value.target_service_accounts, null) + enable_logging = try(each.value.logging, null) + # preview = each.value.preview + description = each.value.description + match { + src_ip_ranges = each.value.direction == "INGRESS" ? each.value.ranges : null + dest_ip_ranges = each.value.direction == "EGRESS" ? each.value.ranges : null + dynamic "layer4_configs" { + for_each = each.value.ports + iterator = port + content { + ip_protocol = port.key + ports = port.value + } + } + } +} + + +resource "google_compute_firewall_policy_association" "association" { + for_each = var.firewall_policy_association + name = replace(local.folder.id, "/", "-") + attachment_target = local.folder.id + firewall_policy = try(google_compute_firewall_policy.policy[each.value].id, each.value) +} + diff --git a/examples/guardrails/gitlab/folder-factory/modules/folder/iam.tf b/examples/guardrails/gitlab/folder-factory/modules/folder/iam.tf new file mode 100644 index 0000000..52886ba --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/modules/folder/iam.tf @@ -0,0 +1,40 @@ +/** + * 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 + * + * 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 IAM bindings, roles and audit logging resources. + +locals { + group_iam_roles = distinct(flatten(values(var.group_iam))) + group_iam = { + for r in local.group_iam_roles : r => [ + for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null + ] + } + iam = { + for role in distinct(concat(keys(var.iam), keys(local.group_iam))) : + role => concat( + try(var.iam[role], []), + try(local.group_iam[role], []) + ) + } +} + +resource "google_folder_iam_binding" "authoritative" { + for_each = local.iam + folder = local.folder.name + role = each.key + members = each.value +} diff --git a/examples/guardrails/gitlab/folder-factory/modules/folder/logging.tf b/examples/guardrails/gitlab/folder-factory/modules/folder/logging.tf new file mode 100644 index 0000000..d6a195e --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/modules/folder/logging.tf @@ -0,0 +1,91 @@ +/** + * 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 + * + * 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 Log sinks and supporting resources. + +locals { + sink_bindings = { + for type in ["bigquery", "pubsub", "logging", "storage"] : + type => { + for name, sink in var.logging_sinks : + name => sink + if sink.type == type + } + } +} + +resource "google_logging_folder_sink" "sink" { + for_each = var.logging_sinks + name = each.key + #description = "${each.key} (Terraform-managed)." + folder = local.folder.name + destination = "${each.value.type}.googleapis.com/${each.value.destination}" + filter = each.value.filter + include_children = each.value.include_children + + dynamic "exclusions" { + for_each = each.value.exclusions + iterator = exclusion + content { + name = exclusion.key + filter = exclusion.value + } + } + + depends_on = [ + google_folder_iam_binding.authoritative + ] +} + +resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { + for_each = local.sink_bindings["storage"] + bucket = each.value.destination + role = "roles/storage.objectCreator" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" { + for_each = local.sink_bindings["bigquery"] + project = split("/", each.value.destination)[1] + dataset_id = split("/", each.value.destination)[3] + role = "roles/bigquery.dataEditor" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" { + for_each = local.sink_bindings["pubsub"] + project = split("/", each.value.destination)[1] + topic = split("/", each.value.destination)[3] + role = "roles/pubsub.publisher" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_project_iam_member" "bucket-sinks-binding" { + for_each = local.sink_bindings["logging"] + project = split("/", each.value.destination)[1] + role = "roles/logging.bucketWriter" + member = google_logging_folder_sink.sink[each.key].writer_identity + # TODO(jccb): use a condition to limit writer-identity only to this + # bucket +} + +resource "google_logging_folder_exclusion" "logging-exclusion" { + for_each = var.logging_exclusions + name = each.key + folder = local.folder.name + description = "${each.key} (Terraform-managed)." + filter = each.value +} diff --git a/examples/guardrails/gitlab/folder-factory/modules/folder/main.tf b/examples/guardrails/gitlab/folder-factory/modules/folder/main.tf new file mode 100644 index 0000000..5d285d2 --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/modules/folder/main.tf @@ -0,0 +1,43 @@ +/** + * 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 + * + * 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 { + folder = ( + var.folder_create + ? try(google_folder.folder.0, null) + : try(data.google_folder.folder.0, null) + ) +} + +data "google_folder" "folder" { + count = var.folder_create ? 0 : 1 + folder = var.id +} + +resource "google_folder" "folder" { + count = var.folder_create ? 1 : 0 + display_name = var.name + parent = var.parent +} + +resource "google_essential_contacts_contact" "contact" { + provider = google-beta + for_each = var.contacts + parent = local.folder.name + email = each.key + language_tag = "en" + notification_category_subscriptions = each.value +} diff --git a/examples/guardrails/gitlab/folder-factory/modules/folder/organization-policies.tf b/examples/guardrails/gitlab/folder-factory/modules/folder/organization-policies.tf new file mode 100644 index 0000000..177a3d8 --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/modules/folder/organization-policies.tf @@ -0,0 +1,90 @@ +/** + * 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 + * + * 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 Folder-level organization policies. + +resource "google_folder_organization_policy" "boolean" { + for_each = var.policy_boolean + folder = local.folder.name + constraint = each.key + + dynamic "boolean_policy" { + for_each = each.value == null ? [] : [each.value] + iterator = policy + content { + enforced = policy.value + } + } + + dynamic "restore_policy" { + for_each = each.value == null ? [""] : [] + content { + default = true + } + } +} + +resource "google_folder_organization_policy" "list" { + for_each = var.policy_list + folder = local.folder.name + constraint = each.key + + dynamic "list_policy" { + for_each = each.value.status == null ? [] : [each.value] + iterator = policy + content { + inherit_from_parent = policy.value.inherit_from_parent + suggested_value = policy.value.suggested_value + dynamic "allow" { + for_each = policy.value.status ? [""] : [] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + dynamic "deny" { + for_each = policy.value.status ? [] : [""] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + } + } + + dynamic "restore_policy" { + for_each = each.value.status == null ? [true] : [] + content { + default = true + } + } +} diff --git a/examples/guardrails/gitlab/folder-factory/modules/folder/outputs.tf b/examples/guardrails/gitlab/folder-factory/modules/folder/outputs.tf new file mode 100644 index 0000000..37babc6 --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/modules/folder/outputs.tf @@ -0,0 +1,51 @@ +/** + * 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 + * + * 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. + */ +output "firewall_policies" { + description = "Map of firewall policy resources created in this folder." + value = { for k, v in google_compute_firewall_policy.policy : k => v } +} + +output "firewall_policy_id" { + description = "Map of firewall policy ids created in this folder." + value = { for k, v in google_compute_firewall_policy.policy : k => v.id } +} + +output "folder" { + description = "Folder resource." + value = local.folder +} + +output "id" { + description = "Folder id." + value = local.folder.name + depends_on = [ + google_folder_iam_binding.authoritative, + google_folder_organization_policy.boolean, + google_folder_organization_policy.list + ] +} + +output "name" { + description = "Folder name." + value = local.folder.display_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 + } +} diff --git a/examples/guardrails/gitlab/folder-factory/modules/folder/tags.tf b/examples/guardrails/gitlab/folder-factory/modules/folder/tags.tf new file mode 100644 index 0000000..2cd2f2f --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/modules/folder/tags.tf @@ -0,0 +1,21 @@ +/** + * 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 + * + * 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. + */ + +resource "google_tags_tag_binding" "binding" { + for_each = coalesce(var.tag_bindings, {}) + parent = "//cloudresourcemanager.googleapis.com/${local.folder.id}" + tag_value = each.value +} diff --git a/examples/guardrails/gitlab/folder-factory/modules/folder/variables.tf b/examples/guardrails/gitlab/folder-factory/modules/folder/variables.tf new file mode 100644 index 0000000..a3f32e3 --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/modules/folder/variables.tf @@ -0,0 +1,151 @@ +/** + * 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 + * + * 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. + */ + +variable "contacts" { + description = "List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES." + type = map(list(string)) + default = {} + nullable = false +} + +variable "firewall_policies" { + description = "Hierarchical firewall policies created in this folder." + type = map(map(object({ + action = string + description = string + direction = string + logging = bool + ports = map(list(string)) + priority = number + ranges = list(string) + target_resources = list(string) + target_service_accounts = list(string) + }))) + default = {} + nullable = false +} + +variable "firewall_policy_association" { + description = "The hierarchical firewall policy to associate to this folder. Must be either a key in the `firewall_policies` map or the id of a policy defined somewhere else." + type = map(string) + default = {} + nullable = false +} + +variable "firewall_policy_factory" { + description = "Configuration for the firewall policy factory." + type = object({ + cidr_file = string + policy_name = string + rules_file = string + }) + default = null +} + +variable "folder_create" { + description = "Create folder. When set to false, uses id to reference an existing folder." + type = bool + default = true +} + +variable "group_iam" { + description = "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." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam" { + description = "IAM bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "id" { + description = "Folder ID in case you use folder_create=false." + type = string + default = null +} + +variable "logging_exclusions" { + description = "Logging exclusions for this folder in the form {NAME -> FILTER}." + type = map(string) + default = {} + nullable = false +} + +variable "logging_sinks" { + description = "Logging sinks to create for this folder." + type = map(object({ + destination = string + type = string + filter = string + include_children = bool + # TODO exclusions also support description and disabled + exclusions = map(string) + })) + validation { + condition = alltrue([ + for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } + default = {} + nullable = false +} + +variable "name" { + description = "Folder name." + type = string + default = null +} + +variable "parent" { + description = "Parent in folders/folder_id or organizations/org_id format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "policy_boolean" { + description = "Map of boolean org policies and enforcement value, set value to null for policy restore." + type = map(bool) + default = {} + nullable = false +} + +variable "policy_list" { + description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." + type = map(object({ + inherit_from_parent = bool + suggested_value = string + status = bool + values = list(string) + })) + default = {} + nullable = false +} + +variable "tag_bindings" { + description = "Tag bindings for this folder, in key => tag value id format." + type = map(string) + default = null +} diff --git a/examples/guardrails/gitlab/folder-factory/modules/folder/versions.tf b/examples/guardrails/gitlab/folder-factory/modules/folder/versions.tf new file mode 100644 index 0000000..e72a780 --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/modules/folder/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.1.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/gitlab/folder-factory/outputs.tf b/examples/guardrails/gitlab/folder-factory/outputs.tf new file mode 100644 index 0000000..a84b9d5 --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/outputs.tf @@ -0,0 +1,20 @@ +/** + * 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 + * + * 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. + */ + +output "folders" { + description = "Created folders." + value = module.folder +} \ No newline at end of file diff --git a/examples/guardrails/gitlab/folder-factory/provider.tf b/examples/guardrails/gitlab/folder-factory/provider.tf new file mode 100644 index 0000000..0e17ef9 --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/provider.tf @@ -0,0 +1,28 @@ +/** + * 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 + * + * 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. + */ + +terraform { + backend "gcs" { + } +} + +provider "google" { + +} + +provider "google-beta" { + +} diff --git a/examples/guardrails/gitlab/folder-factory/variables.tf b/examples/guardrails/gitlab/folder-factory/variables.tf new file mode 100644 index 0000000..156a5ac --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/variables.tf @@ -0,0 +1,15 @@ +/** + * 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 + * + * 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. + */ \ No newline at end of file diff --git a/examples/guardrails/gitlab/project-factory/.gitignore b/examples/guardrails/gitlab/project-factory/.gitignore new file mode 100644 index 0000000..a5d8c0e --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.terraform +.terraform.lock.hcl diff --git a/examples/guardrails/gitlab/project-factory/.gitlab/workflows/.gitlab-ci.yml b/examples/guardrails/gitlab/project-factory/.gitlab/workflows/.gitlab-ci.yml new file mode 100644 index 0000000..dceaa74 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/.gitlab/workflows/.gitlab-ci.yml @@ -0,0 +1,134 @@ +#The pipeline only gets triggered only when a change is committed to one of the following directories mentioned below. +#To update this behavior, add or remove items under changes: section. +workflow: + rules: + - changes: + - "*.tf" + - "*.tfvars" + - "data/**/*" + - "modules/**/*" + +# Workflow image +default: + image: + name: google/cloud-sdk:slim + # pull_policy: if-not-present #Not allowed on the SaaS gitlab runners. + +# Workflow variables. They can be overwritten by passing pipeline Variables in Gitlab repository +variables: + TF_VERSION: $TF_VERSION + TF_ROOT: $TF_ROOT + TF_LOG: $TF_LOG + TF_PLAN_NAME: plan.tfplan + TF_PLAN_JSON: plan.json + REFRESH: -refresh=true + STATE_BUCKET: $STATE_BUCKET + GCP_PROJECT_ID: $GCP_PROJECT_ID + GCP_WORKLOAD_IDENTITY_PROVIDER: $GCP_WORKLOAD_IDENTITY_PROVIDER + GCP_SERVICE_ACCOUNT: $GCP_SERVICE_ACCOUNT + +# Provides a list of stages for this GitLab workflow +stages: + - setup-terraform + - validate + - plan + - apply + +.gcp-auth: &gcp-auth + - echo ${CI_JOB_JWT_V2} > .ci_job_jwt_file + - gcloud iam workload-identity-pools create-cred-config ${GCP_WORKLOAD_IDENTITY_PROVIDER} --service-account="${GCP_SERVICE_ACCOUNT}" --output-file=.gcp_temp_cred.json --credential-source-file=.ci_job_jwt_file + - gcloud auth login --cred-file=`pwd`/.gcp_temp_cred.json + - gcloud config set project $GCP_PROJECT_ID + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/.gcp_temp_cred.json + +.terraform-ver-init: &terraform-ver-init + - cd $TF_ROOT + - cp ./terraform /usr/bin/ + - terraform init -backend-config="bucket=$STATE_BUCKET" -backend-config="prefix=$CI_PROJECT_NAME" --upgrade=True + +# Cache files between jobs +cache: + key: "$CI_COMMIT_SHA" + # Globally caches the .terraform folder across each job in this workflow + paths: + - $TF_ROOT/.terraform + +#Job: setup-terraform | Stage: setup-terraform +# Purpose: downloads specified version of terraform binary and passes it as artifact for the other jobs and stages +setup-terraform: + stage: setup-terraform + script: + - /bin/sh -c 'apt-get update && apt -y install unzip wget && wget https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip && unzip terraform_${TF_VERSION}_linux_amd64.zip' + artifacts: + untracked: false + paths: + - terraform + +#Job: tf-fmt | Stage: validate +# Purpose: check the format (fmt) as a sort of linting test +tf-fmt: + stage: validate + dependencies: + - setup-terraform + before_script: + - *gcp-auth + - *terraform-ver-init + script: + - terraform fmt -recursive -check + allow_failure: false + +# Job: Validate | Stage: Validate +# Purpose: Syntax Validation for the Terraform configuration files +validate: + stage: validate + dependencies: + - setup-terraform + before_script: + - *gcp-auth + - *terraform-ver-init + script: + - terraform validate + allow_failure: false + +#Job: plan | Stage: Plan +#Runs terraform plan and outputs the plan and a json summary to +#local files which are later made available as artifacts. +plan: + stage: plan + dependencies: + - setup-terraform + - validate + before_script: + - *gcp-auth + - *terraform-ver-init + - apt install -y jq + - shopt -s expand_aliases && alias convert_report="jq -r '([.resource_changes[]?.change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'" + script: + - cd $TF_ROOT + - terraform plan -out=$TF_PLAN_NAME $REFRESH + - terraform show --json $TF_PLAN_NAME | convert_report > $TF_PLAN_JSON + allow_failure: false + + artifacts: + reports: + terraform: ${TF_ROOT}/$TF_PLAN_JSON + paths: + - ${TF_ROOT}/$TF_PLAN_NAME + - ${TF_ROOT}/$TF_PLAN_JSON + expire_in: 7 days #optional. Gitlab stores artifacts of successful pipelines for the most recent commit on each ref. If needed, enable "Keep artifacts from most recent successful jobs" in CI/CD settings of the repository. + +#Stage:apply | job: apply +# purpose: executes the plan from the file created in the plan stage +apply: + stage: apply + before_script: + - *gcp-auth + - *terraform-ver-init + dependencies: + - setup-terraform + - plan + script: + - cd $TF_ROOT + - terraform apply -auto-approve $TF_PLAN_NAME + when: manual #Set as manual currently as WIF doesn't support merge request pipelines for now. + allow_failure: false \ No newline at end of file diff --git a/examples/guardrails/gitlab/project-factory/.gitlab/workflows/README.md b/examples/guardrails/gitlab/project-factory/.gitlab/workflows/README.md new file mode 100644 index 0000000..d682232 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/.gitlab/workflows/README.md @@ -0,0 +1,45 @@ + +# Terraform pipeline execution with Gitlab runners and Gitlab repository + +This workflow runs terraform pipelines using Gitlab CICD. This document covers the steps to setup Gitlab CICD and provides a high level overview of the stages involved: + +## Prerequisites + +* A Gitlab project containing the project-factory repository +* Available Gitlab Runners for the project either self hosted or the SaaS Gitlab shared runners. +* A working WIF provider, pool setup with audience as gitlab.com with neccessary attributes and linked service account with required permissions. For further details, please refer to the official [Gitlab Documentation](https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/) + + +## Setup + +1. Update the CICD configuration file path in the repository + * From the project-factory Gitlab project page, Navigate to Settings > CICD > expand General pipelines + * update CI/CD configuration file value to the relative path of the gitlab-ci.yml file from the root directory + +2. Update the CI/CD variables + * From the project-factory project page, Navigate to Settings > CICD > expand Variables + * Add the below variables to the pipeline + +| Variable | Description | Sample value | +|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| GCP_PROJECT_ID | The GCP project ID of your service account | sample-project-1122 | +| GCP_SERVICE_ACCOUNT | The Service Account to be used for creating projects | xyz@sample-project-1122.iam.gserviceaccount.com | +| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects//locations/global/workloadIdentityPools//providers/ | +| STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | +| TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | +| TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | +| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | + +## Overview of the Pipeline stages +The complete workflow consists of 4 stages and 2 before-script jobs + +* before_script jobs : + * gcp-auth : creates the wif credentials by impersonating the service account. + * terraform init : initializes terraform in the specified TF_ROOT directory + +* Stages: + * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages + * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command + * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts + * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once plan is successful. Runs terraform apply and creates the infrastructure specified. + diff --git a/examples/guardrails/gitlab/project-factory/README.md b/examples/guardrails/gitlab/project-factory/README.md new file mode 100644 index 0000000..f901e7f --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/README.md @@ -0,0 +1,75 @@ +# Project Factory + +This is a template for a DevOps project factory. + +It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory) and is intended to house the projects of a specified folder: + +Overview + +Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. + +![Folder Factory](https://user-images.githubusercontent.com/94000358/169809882-f5ff9fb1-d037-49de-8c2c-bf0d457b662f.png) + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: + +Secret settings + +or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: +``` +env: + STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + FOLDER: 'folders/XXXX' + # The folder under which the projects should be created + WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' + # The workload identity provider that should be used for this repository. + SERVICE_ACCOUNT: 'XXXX@XXXX' + # The service account that should be used for this repository. +``` + +## Setting up projects + +The project factory will: +- create a service account with defined rights +- create a project within the folder +- connect the service account to the Github repository informantion + +It uses YAML configuration files for every project with the following sample structure: +``` +billing_account_id: XXXXXX-XXXXXX-XXXXXX +roles: + - roles/viewer + - roles/iam.serviceAccountUser + - roles/iam.securityReviewer + - roles/monitoring.viewer + - roles/monitoring.editor + - roles/monitoring.alertPolicyViewer + - roles/monitoring.alertPolicyEditor + - roles/monitoring.dashboardViewer + - roles/monitoring.dashboardEditor + - roles/monitoring.notificationChannelViewer + - roles/monitoring.notificationChannelEditor + - roles/monitoring.servicesViewer + - roles/monitoring.servicesEditor + - roles/monitoring.uptimeCheckConfigViewer + - roles/monitoring.uptimeCheckConfigEditor + - roles/secretmanager.viewer + - roles/secretmanager.secretVersionManager + - roles/secretmanager.admin + - roles/storage.admin + - roles/storage.objectAdmin + - roles/storage.objectCreator + - roles/storage.objectViewer +repo_provider: github +repo_name: devops-governance/skunkworks +repo_branch: dev +``` + +Every project is defined with its own file located in the [Project Folder](data/projects). diff --git a/examples/guardrails/gitlab/project-factory/data/projects/project.yaml.sample b/examples/guardrails/gitlab/project-factory/data/projects/project.yaml.sample new file mode 100644 index 0000000..d813f4b --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/data/projects/project.yaml.sample @@ -0,0 +1,41 @@ +# 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 +# +# 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. + +billing_account_id: XXXXXX-XXXXXX-XXXXXX +roles: + - roles/viewer + - roles/iam.serviceAccountUser + - roles/iam.securityReviewer + - roles/monitoring.viewer + - roles/monitoring.editor + - roles/monitoring.alertPolicyViewer + - roles/monitoring.alertPolicyEditor + - roles/monitoring.dashboardViewer + - roles/monitoring.dashboardEditor + - roles/monitoring.notificationChannelViewer + - roles/monitoring.notificationChannelEditor + - roles/monitoring.servicesViewer + - roles/monitoring.servicesEditor + - roles/monitoring.uptimeCheckConfigViewer + - roles/monitoring.uptimeCheckConfigEditor + - roles/secretmanager.viewer + - roles/secretmanager.secretVersionManager + - roles/secretmanager.admin + - roles/storage.admin + - roles/storage.objectAdmin + - roles/storage.objectCreator + - roles/storage.objectViewer +repo_provider: github +repo_name: github-org/github-repo +repo_branch: dev diff --git a/examples/guardrails/gitlab/project-factory/main.tf b/examples/guardrails/gitlab/project-factory/main.tf new file mode 100644 index 0000000..1e89f2b --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/main.tf @@ -0,0 +1,35 @@ +/** + * 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 + * + * 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 { + projects = { + for f in fileset("./data/projects", "**/*.yaml") : + trimsuffix(f, ".yaml") => yamldecode(file("./data/projects/${f}")) + } +} + +module "project" { + source = "./modules/project_plus" + for_each = local.projects + team = each.key + repo_sub = each.value.repo_provider == "gitlab" ? "project_path:${each.value.repo_name}:ref_type:branch:ref:${each.value.repo_branch}" : "repo:${each.value.repo_name}:ref:refs/heads/${each.value.repo_branch}" + repo_provider = each.value.repo_provider + billing_account = each.value.billing_account_id + folder = var.folder + roles = try(each.value.roles, []) + wif-pool = each.value.repo_provider == "gitlab" ? google_iam_workload_identity_pool.wif-pool-gitlab.name : google_iam_workload_identity_pool.wif-pool-github.name + depends_on = [google_iam_workload_identity_pool.wif-pool-github,google_iam_workload_identity_pool.wif-pool-gitlab] +} diff --git a/examples/guardrails/gitlab/project-factory/modules/project/README.md b/examples/guardrails/gitlab/project-factory/modules/project/README.md new file mode 100644 index 0000000..e4f2139 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project/README.md @@ -0,0 +1,308 @@ +# Project Module + +## Examples + +### Minimal example with IAM + +```hcl +locals { + gke_service_account = "my_gke_service_account" +} + +module "project" { + source = "./modules/project" + billing_account = "123456-123456-123456" + name = "project-example" + parent = "folders/1234567890" + prefix = "foo" + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + iam = { + "roles/container.hostServiceAgentUser" = [ + "serviceAccount:${local.gke_service_account}" + ] + } +} +# tftest modules=1 resources=4 +``` + +### Minimal example with IAM additive roles + +```hcl +module "project" { + source = "./modules/project" + name = "project-example" + + iam_additive = { + "roles/viewer" = [ + "group:one@example.org", "group:two@xample.org" + ], + "roles/storage.objectAdmin" = [ + "group:two@example.org" + ], + "roles/owner" = [ + "group:three@example.org" + ], + } +} +# tftest modules=1 resources=5 +``` + +### Shared VPC service + +```hcl +module "project" { + source = "./modules/project" + name = "project-example" + + shared_vpc_service_config = { + attach = true + host_project = "my-host-project" + service_identity_iam = { + "roles/compute.networkUser" = [ + "cloudservices", "container-engine" + ] + "roles/vpcaccess.user" = [ + "cloudrun" + ] + "roles/container.hostServiceAgentUser" = [ + "container-engine" + ] + } + } +} +# tftest modules=1 resources=6 +``` + +### Organization policies + +```hcl +module "project" { + source = "./modules/project" + billing_account = "123456-123456-123456" + name = "project-example" + parent = "folders/1234567890" + prefix = "foo" + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + policy_boolean = { + "constraints/compute.disableGuestAttributesAccess" = true + "constraints/compute.skipDefaultNetworkCreation" = true + } + policy_list = { + "constraints/compute.trustedImageProjects" = { + inherit_from_parent = null + suggested_value = null + status = true + values = ["projects/my-project"] + } + } +} +# tftest modules=1 resources=6 +``` + +## Logging Sinks + +```hcl +module "gcs" { + source = "./modules/gcs" + project_id = var.project_id + name = "gcs_sink" + force_destroy = true +} + +module "dataset" { + source = "./modules/bigquery-dataset" + project_id = var.project_id + id = "bq_sink" +} + +module "pubsub" { + source = "./modules/pubsub" + project_id = var.project_id + name = "pubsub_sink" +} + +module "bucket" { + source = "./modules/logging-bucket" + parent_type = "project" + parent = "my-project" + id = "bucket" +} + +module "project-host" { + source = "./modules/project" + name = "my-project" + billing_account = "123456-123456-123456" + parent = "folders/1234567890" + logging_sinks = { + warnings = { + type = "storage" + destination = module.gcs.name + filter = "severity=WARNING" + iam = false + unique_writer = false + exclusions = {} + } + info = { + type = "bigquery" + destination = module.dataset.id + filter = "severity=INFO" + iam = false + unique_writer = false + exclusions = {} + } + notice = { + type = "pubsub" + destination = module.pubsub.id + filter = "severity=NOTICE" + iam = true + unique_writer = false + exclusions = {} + } + debug = { + type = "logging" + destination = module.bucket.id + filter = "severity=DEBUG" + iam = true + unique_writer = false + exclusions = { + no-compute = "logName:compute" + } + } + } + logging_exclusions = { + no-gce-instances = "resource.type=gce_instance" + } +} +# tftest modules=5 resources=12 +``` + +## Cloud KMS encryption keys + +```hcl +module "project" { + source = "./modules/project" + name = "my-project" + billing_account = "123456-123456-123456" + prefix = "foo" + services = [ + "compute.googleapis.com", + "storage.googleapis.com" + ] + service_encryption_key_ids = { + compute = [ + "projects/kms-central-prj/locations/europe-west3/keyRings/my-keyring/cryptoKeys/europe3-gce", + "projects/kms-central-prj/locations/europe-west4/keyRings/my-keyring/cryptoKeys/europe4-gce" + ] + storage = [ + "projects/kms-central-prj/locations/europe/keyRings/my-keyring/cryptoKeys/europe-gcs" + ] + } +} +# tftest modules=1 resources=7 +``` + +## 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. + +```hcl +module "org" { + source = "./modules/organization" + organization_id = var.organization_id + tags = { + environment = { + description = "Environment specification." + iam = null + values = { + dev = null + prod = null + } + } + } +} + +module "project" { + source = "./modules/project" + name = "test-project" + tag_bindings = { + env-prod = module.org.tag_values["environment/prod"].id + foo = "tagValues/12345678" + } +} +# tftest modules=2 resources=6 +``` + + + + +## Files + +| name | description | resources | +|---|---|---| +| [iam.tf](./iam.tf) | Generic and OSLogin-specific IAM bindings and roles. | google_project_iam_binding · google_project_iam_custom_role · google_project_iam_member | +| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_project_exclusion · google_logging_project_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | +| [main.tf](./main.tf) | Module-level locals and resources. | google_compute_project_metadata_item · google_essential_contacts_contact · google_monitoring_monitored_project · google_project · google_project_service · google_resource_manager_lien | +| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_project_organization_policy | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [service-accounts.tf](./service-accounts.tf) | Service identities and supporting resources. | google_kms_crypto_key_iam_member · google_project_service_identity | +| [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project · google_compute_shared_vpc_service_project · google_project_iam_member | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding | +| [variables.tf](./variables.tf) | Module variables. | | +| [versions.tf](./versions.tf) | Version pins. | | +| [vpc-sc.tf](./vpc-sc.tf) | VPC-SC project-level perimeter configuration. | google_access_context_manager_service_perimeter_resource | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [name](variables.tf#L125) | Project name and id suffix. | string | ✓ | | +| [auto_create_network](variables.tf#L17) | Whether to create the default network for the project. | bool | | false | +| [billing_account](variables.tf#L23) | Billing account id. | string | | null | +| [contacts](variables.tf#L29) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | +| [custom_roles](variables.tf#L36) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| [descriptive_name](variables.tf#L43) | Name of the project name. Used for project name instead of `name` variable. | string | | null | +| [group_iam](variables.tf#L49) | 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. | map(list(string)) | | {} | +| [iam](variables.tf#L56) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive](variables.tf#L63) | IAM additive bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive_members](variables.tf#L70) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | map(list(string)) | | {} | +| [labels](variables.tf#L76) | Resource labels. | map(string) | | {} | +| [lien_reason](variables.tf#L83) | If non-empty, creates a project lien with this description. | string | | "" | +| [logging_exclusions](variables.tf#L89) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L96) | Logging sinks to create for this project. | map(object({…})) | | {} | +| [metric_scopes](variables.tf#L118) | List of projects that will act as metric scopes for this project. | list(string) | | [] | +| [oslogin](variables.tf#L130) | Enable OS Login. | bool | | false | +| [oslogin_admins](variables.tf#L136) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | +| [oslogin_users](variables.tf#L144) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | +| [parent](variables.tf#L151) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [policy_boolean](variables.tf#L161) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | +| [policy_list](variables.tf#L168) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | +| [prefix](variables.tf#L180) | Prefix used to generate project id and name. | string | | null | +| [project_create](variables.tf#L186) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | +| [service_config](variables.tf#L192) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L204) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | +| [service_perimeter_bridges](variables.tf#L211) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | +| [service_perimeter_standard](variables.tf#L218) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | +| [services](variables.tf#L224) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L230) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L239) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | +| [skip_delete](variables.tf#L249) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [tag_bindings](variables.tf#L255) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [custom_roles](outputs.tf#L17) | Ids of the created custom roles. | | +| [name](outputs.tf#L25) | Project name. | | +| [number](outputs.tf#L38) | Project number. | | +| [project_id](outputs.tf#L51) | Project id. | | +| [service_accounts](outputs.tf#L66) | Product robot service accounts in project. | | +| [sink_writer_identities](outputs.tf#L82) | Writer identities created for each sink. | | + + diff --git a/examples/guardrails/gitlab/project-factory/modules/project/iam.tf b/examples/guardrails/gitlab/project-factory/modules/project/iam.tf new file mode 100644 index 0000000..69925cc --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project/iam.tf @@ -0,0 +1,115 @@ +/** + * 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 + * + * 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 Generic and OSLogin-specific IAM bindings and roles. + +# IAM notes: +# - external users need to have accepted the invitation email to join +# - oslogin roles also require role to list instances +# - additive (non-authoritative) roles might fail due to dynamic values + +locals { + _group_iam_roles = distinct(flatten(values(var.group_iam))) + _group_iam = { + for r in local._group_iam_roles : r => [ + 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( + try(var.iam[role], []), + 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_project_iam_custom_role" "roles" { + for_each = var.custom_roles + project = local.project.project_id + role_id = each.key + title = "Custom role ${each.key}" + description = "Terraform-managed." + permissions = each.value +} + +resource "google_project_iam_binding" "authoritative" { + for_each = local.iam + project = local.project.project_id + role = each.key + members = each.value + depends_on = [ + google_project_service.project_services, + google_project_iam_custom_role.roles + ] +} + +resource "google_project_iam_member" "additive" { + for_each = ( + length(var.iam_additive) + length(var.iam_additive_members) > 0 + ? local.iam_additive + : {} + ) + project = local.project.project_id + role = each.value.role + member = each.value.member + depends_on = [ + google_project_service.project_services, + google_project_iam_custom_role.roles + ] +} + +resource "google_project_iam_member" "oslogin_iam_serviceaccountuser" { + for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([]) + project = local.project.project_id + role = "roles/iam.serviceAccountUser" + member = each.value +} + +resource "google_project_iam_member" "oslogin_compute_viewer" { + for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([]) + project = local.project.project_id + role = "roles/compute.viewer" + member = each.value +} + +resource "google_project_iam_member" "oslogin_admins" { + for_each = var.oslogin ? toset(var.oslogin_admins) : toset([]) + project = local.project.project_id + role = "roles/compute.osAdminLogin" + member = each.value +} + +resource "google_project_iam_member" "oslogin_users" { + for_each = var.oslogin ? toset(var.oslogin_users) : toset([]) + project = local.project.project_id + role = "roles/compute.osLogin" + member = each.value +} diff --git a/examples/guardrails/gitlab/project-factory/modules/project/logging.tf b/examples/guardrails/gitlab/project-factory/modules/project/logging.tf new file mode 100644 index 0000000..04d7abf --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project/logging.tf @@ -0,0 +1,91 @@ +/** + * 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 + * + * 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 Log sinks and supporting resources. + +locals { + sink_bindings = { + for type in ["bigquery", "pubsub", "logging", "storage"] : + type => { + for name, sink in var.logging_sinks : + name => sink if sink.iam && sink.type == type + } + } +} + +resource "google_logging_project_sink" "sink" { + for_each = var.logging_sinks + name = each.key + #description = "${each.key} (Terraform-managed)." + project = local.project.project_id + destination = "${each.value.type}.googleapis.com/${each.value.destination}" + filter = each.value.filter + unique_writer_identity = each.value.unique_writer + + dynamic "exclusions" { + for_each = each.value.exclusions + iterator = exclusion + content { + name = exclusion.key + filter = exclusion.value + } + } + + depends_on = [ + google_project_iam_binding.authoritative, + google_project_iam_member.additive + ] +} + +resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { + for_each = local.sink_bindings["storage"] + bucket = each.value.destination + role = "roles/storage.objectCreator" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" { + for_each = local.sink_bindings["bigquery"] + project = split("/", each.value.destination)[1] + dataset_id = split("/", each.value.destination)[3] + role = "roles/bigquery.dataEditor" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" { + for_each = local.sink_bindings["pubsub"] + project = split("/", each.value.destination)[1] + topic = split("/", each.value.destination)[3] + role = "roles/pubsub.publisher" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_project_iam_member" "bucket-sinks-binding" { + for_each = local.sink_bindings["logging"] + project = split("/", each.value.destination)[1] + role = "roles/logging.bucketWriter" + member = google_logging_project_sink.sink[each.key].writer_identity + # TODO(jccb): use a condition to limit writer-identity only to this + # bucket +} + +resource "google_logging_project_exclusion" "logging-exclusion" { + for_each = var.logging_exclusions + name = each.key + project = local.project.project_id + description = "${each.key} (Terraform-managed)." + filter = each.value +} diff --git a/examples/guardrails/gitlab/project-factory/modules/project/main.tf b/examples/guardrails/gitlab/project-factory/modules/project/main.tf new file mode 100644 index 0000000..9f0dff4 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project/main.tf @@ -0,0 +1,95 @@ +/** + * 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 + * + * 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 { + descriptive_name = ( + var.descriptive_name != null ? var.descriptive_name : "${local.prefix}${var.name}" + ) + parent_type = var.parent == null ? null : split("/", var.parent)[0] + parent_id = var.parent == null ? null : split("/", var.parent)[1] + prefix = var.prefix == null ? "" : "${var.prefix}-" + project = ( + var.project_create ? + { + project_id = try(google_project.project.0.project_id, null) + number = try(google_project.project.0.number, null) + name = try(google_project.project.0.name, null) + } + : { + project_id = "${local.prefix}${var.name}" + number = try(data.google_project.project.0.number, null) + name = try(data.google_project.project.0.name, null) + } + ) +} + +data "google_project" "project" { + count = var.project_create ? 0 : 1 + project_id = "${local.prefix}${var.name}" +} + +resource "google_project" "project" { + count = var.project_create ? 1 : 0 + org_id = local.parent_type == "organizations" ? local.parent_id : null + folder_id = local.parent_type == "folders" ? local.parent_id : null + project_id = "${local.prefix}${var.name}" + name = local.descriptive_name + billing_account = var.billing_account + auto_create_network = var.auto_create_network + labels = var.labels + skip_delete = var.skip_delete +} + +resource "google_project_service" "project_services" { + for_each = toset(var.services) + project = local.project.project_id + service = each.value + disable_on_destroy = var.service_config.disable_on_destroy + disable_dependent_services = var.service_config.disable_dependent_services +} + +resource "google_compute_project_metadata_item" "oslogin_meta" { + count = var.oslogin ? 1 : 0 + project = local.project.project_id + key = "enable-oslogin" + value = "TRUE" + # depend on services or it will fail on destroy + depends_on = [google_project_service.project_services] +} + +resource "google_resource_manager_lien" "lien" { + count = var.lien_reason != "" ? 1 : 0 + parent = "projects/${local.project.number}" + restrictions = ["resourcemanager.projects.delete"] + origin = "created-by-terraform" + reason = var.lien_reason +} + +resource "google_essential_contacts_contact" "contact" { + provider = google-beta + for_each = var.contacts + parent = "projects/${local.project.project_id}" + email = each.key + language_tag = "en" + notification_category_subscriptions = each.value +} + +resource "google_monitoring_monitored_project" "primary" { + provider = google-beta + for_each = toset(var.metric_scopes) + metrics_scope = each.value + name = local.project.project_id +} diff --git a/examples/guardrails/gitlab/project-factory/modules/project/organization-policies.tf b/examples/guardrails/gitlab/project-factory/modules/project/organization-policies.tf new file mode 100644 index 0000000..6870754 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project/organization-policies.tf @@ -0,0 +1,90 @@ +/** + * 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 + * + * 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 Project-level organization policies. + +resource "google_project_organization_policy" "boolean" { + for_each = var.policy_boolean + project = local.project.project_id + constraint = each.key + + dynamic "boolean_policy" { + for_each = each.value == null ? [] : [each.value] + iterator = policy + content { + enforced = policy.value + } + } + + dynamic "restore_policy" { + for_each = each.value == null ? [""] : [] + content { + default = true + } + } +} + +resource "google_project_organization_policy" "list" { + for_each = var.policy_list + project = local.project.project_id + constraint = each.key + + dynamic "list_policy" { + for_each = each.value.status == null ? [] : [each.value] + iterator = policy + content { + inherit_from_parent = policy.value.inherit_from_parent + suggested_value = policy.value.suggested_value + dynamic "allow" { + for_each = policy.value.status ? [""] : [] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + dynamic "deny" { + for_each = policy.value.status ? [] : [""] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + } + } + + dynamic "restore_policy" { + for_each = each.value.status == null ? [true] : [] + content { + default = true + } + } +} diff --git a/examples/guardrails/gitlab/project-factory/modules/project/outputs.tf b/examples/guardrails/gitlab/project-factory/modules/project/outputs.tf new file mode 100644 index 0000000..10d0e55 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project/outputs.tf @@ -0,0 +1,87 @@ +/** + * 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 + * + * 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. + */ + +output "custom_roles" { + description = "Ids of the created custom roles." + value = { + for name, role in google_project_iam_custom_role.roles : + name => role.id + } +} + +output "name" { + description = "Project name." + value = local.project.name + depends_on = [ + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "number" { + description = "Project number." + value = local.project.number + depends_on = [ + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "project_id" { + description = "Project id." + value = "${local.prefix}${var.name}" + depends_on = [ + google_project.project, + data.google_project.project, + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "service_accounts" { + description = "Product robot service accounts in project." + value = { + cloud_services = local.service_account_cloud_services + default = local.service_accounts_default + robots = local.service_accounts_robots + } + depends_on = [ + google_project_service.project_services, + google_kms_crypto_key_iam_member.service_identity_cmek, + google_project_service_identity.jit_si, + data.google_bigquery_default_service_account.bq_sa, + data.google_storage_project_service_account.gcs_sa + ] +} + +output "sink_writer_identities" { + description = "Writer identities created for each sink." + value = { + for name, sink in google_logging_project_sink.sink : name => sink.writer_identity + } +} diff --git a/examples/guardrails/gitlab/project-factory/modules/project/service-accounts.tf b/examples/guardrails/gitlab/project-factory/modules/project/service-accounts.tf new file mode 100644 index 0000000..3423524 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project/service-accounts.tf @@ -0,0 +1,113 @@ +/** + * 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 + * + * 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 Service identities and supporting resources. + +locals { + _service_accounts_cmek_service_dependencies = { + "composer" : [ + "composer", + "artifactregistry", "container-engine", "compute", "pubsub", "storage" + ] + "dataflow" : ["dataflow", "compute"] + } + _service_accounts_robot_services = { + artifactregistry = "service-%s@gcp-sa-artifactregistry" + bq = "bq-%s@bigquery-encryption" + cloudasset = "service-%s@gcp-sa-cloudasset" + cloudbuild = "service-%s@gcp-sa-cloudbuild" + cloudfunctions = "service-%s@gcf-admin-robot" + cloudrun = "service-%s@serverless-robot-prod" + composer = "service-%s@cloudcomposer-accounts" + compute = "service-%s@compute-system" + container-engine = "service-%s@container-engine-robot" + containerregistry = "service-%s@containerregistry" + dataflow = "service-%s@dataflow-service-producer-prod" + dataproc = "service-%s@dataproc-accounts" + gae-flex = "service-%s@gae-api-prod" + # TODO: deprecate gcf + gcf = "service-%s@gcf-admin-robot" + pubsub = "service-%s@gcp-sa-pubsub" + secretmanager = "service-%s@gcp-sa-secretmanager" + storage = "service-%s@gs-project-accounts" + } + service_accounts_default = { + compute = "${local.project.number}-compute@developer.gserviceaccount.com" + gae = "${local.project.project_id}@appspot.gserviceaccount.com" + } + service_account_cloud_services = ( + "${local.project.number}@cloudservices.gserviceaccount.com" + ) + service_accounts_robots = { + for k, v in local._service_accounts_robot_services : + k => "${format(v, local.project.number)}.iam.gserviceaccount.com" + } + service_accounts_jit_services = [ + "secretmanager.googleapis.com", + "pubsub.googleapis.com", + "cloudasset.googleapis.com" + ] + service_accounts_cmek_service_keys = distinct(flatten([ + for s in keys(var.service_encryption_key_ids) : [ + for ss in try(local._service_accounts_cmek_service_dependencies[s], [s]) : [ + for key in var.service_encryption_key_ids[s] : { + service = ss + key = key + } if key != null + ] + ] + ])) +} + +data "google_storage_project_service_account" "gcs_sa" { + count = contains(var.services, "storage.googleapis.com") ? 1 : 0 + project = local.project.project_id + depends_on = [google_project_service.project_services] +} + +data "google_bigquery_default_service_account" "bq_sa" { + count = contains(var.services, "bigquery.googleapis.com") ? 1 : 0 + project = local.project.project_id + depends_on = [google_project_service.project_services] +} + +# Secret Manager SA created just in time, we need to trigger the creation. +resource "google_project_service_identity" "jit_si" { + for_each = setintersection(var.services, local.service_accounts_jit_services) + provider = google-beta + project = local.project.project_id + service = each.value + depends_on = [google_project_service.project_services] +} + +resource "google_kms_crypto_key_iam_member" "service_identity_cmek" { + for_each = { + for service_key in local.service_accounts_cmek_service_keys : + "${service_key.service}.${service_key.key}" => service_key + if service_key != service_key.key + } + crypto_key_id = each.value.key + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + member = "serviceAccount:${local.service_accounts_robots[each.value.service]}" + depends_on = [ + google_project.project, + google_project_service.project_services, + google_project_service_identity.jit_si, + data.google_bigquery_default_service_account.bq_sa, + data.google_project.project, + data.google_storage_project_service_account.gcs_sa, + ] +} diff --git a/examples/guardrails/gitlab/project-factory/modules/project/shared-vpc.tf b/examples/guardrails/gitlab/project-factory/modules/project/shared-vpc.tf new file mode 100644 index 0000000..9c7bd71 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project/shared-vpc.tf @@ -0,0 +1,75 @@ +/** + * 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 + * + * 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 Shared VPC project-level configuration. + +locals { + # compute the host project IAM bindings for this project's service identities + _svpc_service_identity_iam = coalesce( + local.svpc_service_config.service_identity_iam, {} + ) + _svpc_service_iam = flatten([ + for role, services in local._svpc_service_identity_iam : [ + for service in services : { role = role, service = service } + ] + ]) + svpc_host_config = { + enabled = coalesce( + try(var.shared_vpc_host_config.enabled, null), false + ) + service_projects = coalesce( + try(var.shared_vpc_host_config.service_projects, null), [] + ) + } + svpc_service_config = coalesce(var.shared_vpc_service_config, { + host_project = null, service_identity_iam = {} + }) + svpc_service_iam = { + for b in local._svpc_service_iam : "${b.role}:${b.service}" => b + } +} + +resource "google_compute_shared_vpc_host_project" "shared_vpc_host" { + provider = google-beta + count = local.svpc_host_config.enabled ? 1 : 0 + project = local.project.project_id +} + +resource "google_compute_shared_vpc_service_project" "service_projects" { + provider = google-beta + for_each = toset(local.svpc_host_config.service_projects) + host_project = local.project.project_id + service_project = each.value + depends_on = [google_compute_shared_vpc_host_project.shared_vpc_host] +} + +resource "google_compute_shared_vpc_service_project" "shared_vpc_service" { + provider = google-beta + count = local.svpc_service_config.host_project != null ? 1 : 0 + host_project = var.shared_vpc_service_config.host_project + service_project = local.project.project_id +} + +resource "google_project_iam_member" "shared_vpc_host_robots" { + for_each = local.svpc_service_iam + project = var.shared_vpc_service_config.host_project + role = each.value.role + member = ( + each.value.service == "cloudservices" + ? "serviceAccount:${local.service_account_cloud_services}" + : "serviceAccount:${local.service_accounts_robots[each.value.service]}" + ) +} diff --git a/examples/guardrails/gitlab/project-factory/modules/project/tags.tf b/examples/guardrails/gitlab/project-factory/modules/project/tags.tf new file mode 100644 index 0000000..683143b --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project/tags.tf @@ -0,0 +1,21 @@ +/** + * 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 + * + * 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. + */ + +resource "google_tags_tag_binding" "binding" { + for_each = coalesce(var.tag_bindings, {}) + parent = "//cloudresourcemanager.googleapis.com/projects/${local.project.number}" + tag_value = each.value +} diff --git a/examples/guardrails/gitlab/project-factory/modules/project/variables.tf b/examples/guardrails/gitlab/project-factory/modules/project/variables.tf new file mode 100644 index 0000000..578f9d2 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project/variables.tf @@ -0,0 +1,259 @@ +/** + * 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 + * + * 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. + */ + +variable "auto_create_network" { + description = "Whether to create the default network for the project." + type = bool + default = false +} + +variable "billing_account" { + description = "Billing account id." + type = string + default = null +} + +variable "contacts" { + description = "List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES." + type = map(list(string)) + default = {} + nullable = false +} + +variable "custom_roles" { + description = "Map of role name => list of permissions to create in this project." + type = map(list(string)) + default = {} + nullable = false +} + +variable "descriptive_name" { + description = "Name of the project name. Used for project name instead of `name` variable." + type = string + default = null +} + +variable "group_iam" { + description = "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." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam" { + description = "IAM bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_additive" { + description = "IAM additive bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +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 "labels" { + description = "Resource labels." + type = map(string) + default = {} + nullable = false +} + +variable "lien_reason" { + description = "If non-empty, creates a project lien with this description." + type = string + default = "" +} + +variable "logging_exclusions" { + description = "Logging exclusions for this project in the form {NAME -> FILTER}." + type = map(string) + default = {} + nullable = false +} + +variable "logging_sinks" { + description = "Logging sinks to create for this project." + type = map(object({ + destination = string + type = string + filter = string + iam = bool + unique_writer = bool + # TODO exclusions also support description and disabled + exclusions = map(string) + })) + validation { + condition = alltrue([ + for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } + default = {} + nullable = false +} + +variable "metric_scopes" { + description = "List of projects that will act as metric scopes for this project." + type = list(string) + default = [] + nullable = false +} + +variable "name" { + description = "Project name and id suffix." + type = string +} + +variable "oslogin" { + description = "Enable OS Login." + type = bool + default = false +} + +variable "oslogin_admins" { + description = "List of IAM-style identities that will be granted roles necessary for OS Login administrators." + type = list(string) + default = [] + nullable = false + +} + +variable "oslogin_users" { + description = "List of IAM-style identities that will be granted roles necessary for OS Login users." + type = list(string) + default = [] + nullable = false +} + +variable "parent" { + description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "policy_boolean" { + description = "Map of boolean org policies and enforcement value, set value to null for policy restore." + type = map(bool) + default = {} + nullable = false +} + +variable "policy_list" { + description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." + type = map(object({ + inherit_from_parent = bool + suggested_value = string + status = bool + values = list(string) + })) + default = {} + nullable = false +} + +variable "prefix" { + description = "Prefix used to generate project id and name." + type = string + default = null +} + +variable "project_create" { + description = "Create project. When set to false, uses a data source to reference existing project." + type = bool + default = true +} + +variable "service_config" { + description = "Configure service API activation." + type = object({ + disable_on_destroy = bool + disable_dependent_services = bool + }) + default = { + disable_on_destroy = true + disable_dependent_services = true + } +} + +variable "service_encryption_key_ids" { + description = "Cloud KMS encryption key in {SERVICE => [KEY_URL]} format." + type = map(list(string)) + default = {} +} + +# accessPolicies/ACCESS_POLICY_NAME/servicePerimeters/PERIMETER_NAME +variable "service_perimeter_bridges" { + description = "Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format." + type = list(string) + default = null +} + +# accessPolicies/ACCESS_POLICY_NAME/servicePerimeters/PERIMETER_NAME +variable "service_perimeter_standard" { + description = "Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format." + type = string + default = null +} + +variable "services" { + description = "Service APIs to enable." + type = list(string) + default = [] +} + +variable "shared_vpc_host_config" { + description = "Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project)." + type = object({ + enabled = bool + service_projects = list(string) + }) + default = null +} + +variable "shared_vpc_service_config" { + description = "Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config)." + # the list of valid service identities is in service-accounts.tf + type = object({ + host_project = string + service_identity_iam = map(list(string)) + }) + default = null +} + +variable "skip_delete" { + description = "Allows the underlying resources to be destroyed without destroying the project itself." + type = bool + default = false +} + +variable "tag_bindings" { + description = "Tag bindings for this project, in key => tag value id format." + type = map(string) + default = null +} diff --git a/examples/guardrails/gitlab/project-factory/modules/project/versions.tf b/examples/guardrails/gitlab/project-factory/modules/project/versions.tf new file mode 100644 index 0000000..e72a780 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.1.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/gitlab/project-factory/modules/project/vpc-sc.tf b/examples/guardrails/gitlab/project-factory/modules/project/vpc-sc.tf new file mode 100644 index 0000000..edaa203 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project/vpc-sc.tf @@ -0,0 +1,45 @@ +/** + * 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 + * + * 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 VPC-SC project-level perimeter configuration. + +moved { + from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-standard + to = google_access_context_manager_service_perimeter_resource.standard +} + +resource "google_access_context_manager_service_perimeter_resource" "standard" { + count = var.service_perimeter_standard != null ? 1 : 0 + # this needs an additional lifecycle block in the vpc module on the + # google_access_context_manager_service_perimeter resource + perimeter_name = var.service_perimeter_standard + resource = "projects/${local.project.number}" +} + +moved { + from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-bridges + to = google_access_context_manager_service_perimeter_resource.bridge +} + +resource "google_access_context_manager_service_perimeter_resource" "bridge" { + for_each = toset( + var.service_perimeter_bridges != null ? var.service_perimeter_bridges : [] + ) + # this needs an additional lifecycle block in the vpc module on the + # google_access_context_manager_service_perimeter resource + perimeter_name = each.value + resource = "projects/${local.project.number}" +} diff --git a/examples/guardrails/gitlab/project-factory/modules/project_plus/README.md b/examples/guardrails/gitlab/project-factory/modules/project_plus/README.md new file mode 100644 index 0000000..2976a30 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project_plus/README.md @@ -0,0 +1 @@ +This is an addon for the project module with connects one service account to one project. \ No newline at end of file diff --git a/examples/guardrails/gitlab/project-factory/modules/project_plus/main.tf b/examples/guardrails/gitlab/project-factory/modules/project_plus/main.tf new file mode 100644 index 0000000..7330dd0 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project_plus/main.tf @@ -0,0 +1,46 @@ +/** + * 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 + * + * 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. + */ + +resource "random_id" "rand" { + byte_length = 4 +} + +module "project" { + source = "./../project" + name = "${var.team}-prj-${random_id.rand.hex}" + parent = var.folder + billing_account = var.billing_account +} + +resource "google_service_account" "sa" { + account_id = "${var.team}-sa-${random_id.rand.hex}" + display_name = "Service account ${var.team}" + project = module.project.project_id +} + +resource "google_service_account_iam_member" "sa-iam" { + service_account_id = google_service_account.sa.name + role = "roles/iam.workloadIdentityUser" + member = "principalSet://iam.googleapis.com/${var.wif-pool}/attribute.sub/${var.repo_sub}" +} + +resource "google_project_iam_member" "sa-project" { + for_each = toset(var.roles) + role = each.value + member = "serviceAccount:${google_service_account.sa.email}" + project = module.project.project_id + depends_on = [google_service_account.sa] +} diff --git a/examples/guardrails/gitlab/project-factory/modules/project_plus/outputs.tf b/examples/guardrails/gitlab/project-factory/modules/project_plus/outputs.tf new file mode 100644 index 0000000..c589d87 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project_plus/outputs.tf @@ -0,0 +1,36 @@ +/** + * 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 + * + * 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. + */ + +output "project_id" { + description = "Project ID" + value = module.project.project_id +} + +output "service_account_email" { + description = "Service Account Email" + value = google_service_account.sa.email +} + +output "repo_sub" { + description = "Repository" + value = var.repo_sub +} + +output "repo_provider" { + description = "Repository Provider" + value = var.repo_provider +} + diff --git a/examples/guardrails/gitlab/project-factory/modules/project_plus/variables.tf b/examples/guardrails/gitlab/project-factory/modules/project_plus/variables.tf new file mode 100644 index 0000000..67524ae --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project_plus/variables.tf @@ -0,0 +1,52 @@ +/** + * 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 + * + * 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. + */ + + +variable "team" { + description = "Team name." + type = string +} + +variable "repo_sub" { + description = "Repository path" + type = string +} + +variable "repo_provider" { + description = "Repository provider" + type = string +} + +variable "billing_account" { + description = "Billing account name." + type = string +} + +variable "folder" { + description = "Folder name." + type = string +} + +variable "roles" { + description = "Roles to attach." + type = list(string) + default = [] +} + +variable "wif-pool" { + description = "WIF pool name." + type = string +} diff --git a/examples/guardrails/gitlab/project-factory/modules/project_plus/versions.tf b/examples/guardrails/gitlab/project-factory/modules/project_plus/versions.tf new file mode 100644 index 0000000..2904126 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/modules/project_plus/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.0.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/gitlab/project-factory/outputs.tf b/examples/guardrails/gitlab/project-factory/outputs.tf new file mode 100644 index 0000000..43d9c4f --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/outputs.tf @@ -0,0 +1,40 @@ +/** + * 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 + * + * 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. + */ + +output "wif_pool_id_gitlab" { + description = "Gitlab Workload Identity Pool" + value = google_iam_workload_identity_pool.wif-pool-gitlab.name +} + +output "wif_provider_id_gitlab" { + description = "Gitlab Workload Identity Pool" + value = google_iam_workload_identity_pool_provider.wif-provider-gitlab.name +} + +output "wif_pool_id_github" { + description = "Github Workload Identity Pool" + value = google_iam_workload_identity_pool.wif-pool-github.name +} + +output "wif_provider_id_github" { + description = "Github Workload Identity Pool" + value = google_iam_workload_identity_pool_provider.wif-provider-github.name +} + +output "projects" { + description = "Created projects and service accounts." + value = module.project +} \ No newline at end of file diff --git a/examples/guardrails/gitlab/project-factory/provider.tf b/examples/guardrails/gitlab/project-factory/provider.tf new file mode 100644 index 0000000..34beb2f --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/provider.tf @@ -0,0 +1,28 @@ +/** + * 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 + * + * 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. + */ + +terraform { + backend "gcs" { + } +} + +provider "google" { + +} + +provider "google-beta" { + +} diff --git a/examples/guardrails/gitlab/project-factory/variables.tf b/examples/guardrails/gitlab/project-factory/variables.tf new file mode 100644 index 0000000..df6e79d --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/variables.tf @@ -0,0 +1,19 @@ +/** + * 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 + * + * 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. + */ + +variable "folder" { + +} diff --git a/examples/guardrails/gitlab/project-factory/wif.tf b/examples/guardrails/gitlab/project-factory/wif.tf new file mode 100644 index 0000000..7201ae9 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/wif.tf @@ -0,0 +1,68 @@ +/** + * 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 + * + * 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. + */ + +resource "random_id" "rand" { + byte_length = 4 +} + +module "wif-project" { + source = "./modules/project" + name = "wif-prj-${random_id.rand.hex}" + parent = var.folder + billing_account = "01B3B2-962224-4EEC67" +} + +resource "google_iam_workload_identity_pool" "wif-pool-gitlab" { + provider = google-beta + workload_identity_pool_id = "gitlab-pool-${random_id.rand.hex}" + project = module.wif-project.project_id +} + +resource "google_iam_workload_identity_pool_provider" "wif-provider-gitlab" { + provider = google-beta + workload_identity_pool_id = google_iam_workload_identity_pool.wif-pool-gitlab.workload_identity_pool_id + workload_identity_pool_provider_id = "gitlab-provider-${random_id.rand.hex}" + project = module.wif-project.project_id + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.sub" = "assertion.sub" + } + oidc { + issuer_uri = "https://gitlab.com/" + allowed_audiences = ["https://gitlab.com"] + } +} + +resource "google_iam_workload_identity_pool" "wif-pool-github" { + provider = google-beta + workload_identity_pool_id = "github-pool-${random_id.rand.hex}" + project = module.wif-project.project_id +} + +resource "google_iam_workload_identity_pool_provider" "wif-provider-github" { + provider = google-beta + workload_identity_pool_id = google_iam_workload_identity_pool.wif-pool-github.workload_identity_pool_id + workload_identity_pool_provider_id = "github-provider-${random_id.rand.hex}" + project = module.wif-project.project_id + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.sub" = "assertion.sub" + "attribute.actor" = "assertion.actor" + } + oidc { + issuer_uri = "https://token.actions.githubusercontent.com" + } +} diff --git a/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/.gitlab-ci.yml b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/.gitlab-ci.yml new file mode 100644 index 0000000..ed1219c --- /dev/null +++ b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/.gitlab-ci.yml @@ -0,0 +1,46 @@ +stages: + - triggers + +variables: + STATE_BUCKET: $STATE_BUCKET + GCP_WORKLOAD_IDENTITY_PROVIDER: $GCP_WORKLOAD_IDENTITY_PROVIDER + TF_VERSION: $TF_VERSION + TF_LOG: $TF_LOG + TF_ROOT: $TF_ROOT + +trigger_dev: + variables: + GCP_PROJECT_ID: $DEV_GCP_PROJECT_ID + GCP_SERVICE_ACCOUNT: $DEV_GCP_SERVICE_ACCOUNT + ENVIRONMENT: dev + stage: triggers + trigger: + include: .gitlab/workflows/workflow.yml + strategy: depend + only: + - dev + +trigger_staging: + stage: triggers + variables: + GCP_PROJECT_ID: $STAGE_GCP_PROJECT_ID + GCP_SERVICE_ACCOUNT: $STAGE_GCP_SERVICE_ACCOUNT + ENVIRONMENT: staging + trigger: + include: .gitlab/workflows/workflow.yml + strategy: depend + only: + - staging + +trigger_prod: + stage: triggers + variables: + GCP_PROJECT_ID: $PROD_PROJECT_ID + GCP_SERVICE_ACCOUNT: $PROD_GCP_SERVICE_ACCOUNT + ENVIRONMENT: prod + trigger: + include: .gitlab/workflows/workflow.yml + strategy: depend + only: + - prod + \ No newline at end of file diff --git a/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/README.md b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/README.md new file mode 100644 index 0000000..27903ae --- /dev/null +++ b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/README.md @@ -0,0 +1,54 @@ + +# Terraform pipeline execution with Gitlab runners and Gitlab repository + +This workflow runs terraform pipelines using Gitlab CICD. This document covers the steps to setup Gitlab CICD and provides a high level overview of the stages involved: + +## Prerequisites + +* A Gitlab project containing the project-factory repository +* Available Gitlab Runners for the project either self hosted or the SaaS Gitlab shared runners. +* A working WIF provider, pool setup with audience as gitlab.com with neccessary attributes and linked service account with required permissions. For further details, please refer to the official [Gitlab Documentation](https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/) + + + +## Overview +This workflow can execute terraform on different GCP projects depending on the branch on which push event is executed. Supports dev, staging and prod branches. The .gitlab-ci.yml file triggers the child pipeline on the respective branches and runs the workflow as specified in the workflow.yml file to execute terraform. All the three branches dev, staging and prod are linked to specific projects with DEV_, STAGE_, PROD_ CI/CD variables as specified in Gitlab project. For more information, see the setup section below + + +## Setup + +1. Update the CICD configuration file path in the repository + * From the skunkworks Gitlab project page, Navigate to Settings > CICD > expand General pipelines + * update CI/CD configuration file value to the relative path of the gitlab-ci.yml file from the root directory + +2. Update the CI/CD variables + * From the skunkworks project page, Navigate to Settings > CICD > expand Variables + * Add the below variables to the pipeline + +| Variable | Description | Sample value | +|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| DEV_GCP_PROJECT_ID | The GCP project ID in which resources are to be created on a push event to dev branch | sample-dev-project-1122 | +| DEV_GCP_SERVICE_ACCOUNT | The Service Account of the dev gcp project configured with Workload Identity Federation (WIF) | xyz@sample-dev-project-1122.iam.gserviceaccount.com | +| STAGE_GCP_PROJECT_ID | The GCP project ID in which resources are to be created on a push event to staging branch | sample-stage-project-1122 | +| STAGE_GCP_SERVICE_ACCOUNT | The Service Account of the staging gcp project configured with Workload Identity Federation (WIF) | xyz@sample-stage-project-1122.iam.gserviceaccount.com | +| PROD_GCP_PROJECT_ID | The GCP project ID in which resources are to be created on a push event to prod branch | sample-prod-project-1122 | +| PROD_GCP_SERVICE_ACCOUNT | The Service Account of the prod gcp project configured with Workload Identity Federation (WIF) | xyz@sample-prod-project-1122.iam.gserviceaccount.com | +| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects//locations/global/workloadIdentityPools//providers/ | +| STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | +| TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | +| TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | +| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | | + +## Overview of the Pipeline stages +The complete workflow consists of 4 stages and 2 before-script jobs + +* before_script jobs : + * gcp-auth : creates the wif credentials by impersonating the service account. + * terraform init : initializes terraform in the specified TF_ROOT directory + +* Stages: + * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages + * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command + * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts + * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once plan is successful. Runs terraform apply and creates the infrastructure specified. + diff --git a/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/workflow.yml b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/workflow.yml new file mode 100644 index 0000000..9059561 --- /dev/null +++ b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/workflow.yml @@ -0,0 +1,116 @@ +# Workflow image +default: + image: + name: google/cloud-sdk:slim + # pull_policy: if-not-present #Not allowed on the SaaS gitlab runners. + +# Workflow variables. They can be overwritten by passing pipeline Variables in Gitlab repository +variables: + TF_PLAN_NAME: plan.tfplan + TF_PLAN_JSON: plan.json + REFRESH: -refresh=true + +# Provides a list of stages for this GitLab workflow +stages: + - setup-terraform + - validate + - plan + - apply + +.gcp-auth: &gcp-auth + - echo ${CI_JOB_JWT_V2} > .ci_job_jwt_file + - gcloud iam workload-identity-pools create-cred-config ${GCP_WORKLOAD_IDENTITY_PROVIDER} --service-account="${GCP_SERVICE_ACCOUNT}" --output-file=.gcp_temp_cred.json --credential-source-file=.ci_job_jwt_file + - gcloud auth login --cred-file=`pwd`/.gcp_temp_cred.json + - gcloud config set project $GCP_PROJECT_ID + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/.gcp_temp_cred.json + +.terraform-ver-init: &terraform-ver-init + - cd $TF_ROOT + - cp ./terraform /usr/bin/ + - terraform init -backend-config="bucket=$STATE_BUCKET" -backend-config="prefix=$CI_PROJECT_NAME/$GCP_PROJECT_ID" --upgrade=True + +# Cache files between jobs +cache: + key: "$CI_COMMIT_SHA" + # Globally caches the .terraform folder across each job in this workflow + paths: + - $TF_ROOT/.terraform + +setup-terraform: + stage: setup-terraform + script: + - /bin/sh -c 'apt-get update && apt -y install unzip wget && wget https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip && unzip terraform_${TF_VERSION}_linux_amd64.zip' + artifacts: + untracked: false + paths: + - terraform + +#Job: tf-fmt | Stage: validate +# Purpose: check the format (fmt) as a sort of linting test +tf-fmt: + stage: validate + dependencies: + - setup-terraform + before_script: + - *gcp-auth + - *terraform-ver-init + script: + - terraform fmt -recursive -check + allow_failure: false + +# Job: Validate | Stage: Validate +# Purpose: Syntax Validation for the Terraform configuration files +validate: + stage: validate + dependencies: + - setup-terraform + before_script: + - *gcp-auth + - *terraform-ver-init + script: + - terraform validate + allow_failure: false + +#Job: plan | Stage: Plan +#Runs terraform plan and outputs the plan and a json summary to +#local files which are later made available as artifacts. +plan: + stage: plan + dependencies: + - setup-terraform + - validate + before_script: + - *gcp-auth + - *terraform-ver-init + - apt install -y jq + - shopt -s expand_aliases && alias convert_report="jq -r '([.resource_changes[]?.change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'" + script: + - cd $TF_ROOT + - terraform plan -out=$TF_PLAN_NAME $REFRESH + - terraform show --json $TF_PLAN_NAME | convert_report > $TF_PLAN_JSON + allow_failure: false + + artifacts: + reports: + terraform: ${TF_ROOT}/$TF_PLAN_JSON + paths: + - ${TF_ROOT}/$TF_PLAN_NAME + - ${TF_ROOT}/$TF_PLAN_JSON + expire_in: 7 days #optional. Gitlab stores artifacts of successful pipelines for the most recent commit on each ref. If needed, enable "Keep artifacts from most recent successful jobs" in CI/CD settings of the repository. + +#Stage:apply | job: apply +# purpose: executes the plan from the file created in the plan stage +apply: + stage: apply + environment: $ENVIRONMENT + before_script: + - *gcp-auth + - *terraform-ver-init + dependencies: + - setup-terraform + - plan + script: + - cd $TF_ROOT + - terraform apply -auto-approve $TF_PLAN_NAME + when: manual #Set as manual currently as WIF doesn't support merge request pipelines for now. + allow_failure: false \ No newline at end of file diff --git a/examples/guardrails/gitlab/skunkworks/README.md b/examples/guardrails/gitlab/skunkworks/README.md new file mode 100644 index 0000000..0b89bb2 --- /dev/null +++ b/examples/guardrails/gitlab/skunkworks/README.md @@ -0,0 +1,29 @@ +# Skunkworks - IaC Kickstarter Template + +This is a template for an IaC kickstarter repository. + +![Skunkworks](https://user-images.githubusercontent.com/94000358/169810982-36f01de2-e5e5-4ecd-b98e-3cf5a6aa9f81.png) + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +This template creates a bucket in the specified target environment. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: + +Secret configuration + +or by modifing the [Workflow Action Files](.github/workflows/) and setting the environment variables: +``` +env: + STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' + # The workload identity provider that should be used for this repository. + SERVICE_ACCOUNT: 'XXXX@XXXX' + # The service account that should be used for this repository. +``` diff --git a/examples/guardrails/gitlab/skunkworks/main.tf b/examples/guardrails/gitlab/skunkworks/main.tf new file mode 100644 index 0000000..03926bd --- /dev/null +++ b/examples/guardrails/gitlab/skunkworks/main.tf @@ -0,0 +1,23 @@ +/** + * 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 + * + * 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. + */ + +resource "google_storage_bucket" "bucket" { + project = var.project + name = lower("${var.project}-test-bucket") + location = "EU" + force_destroy = true +} + diff --git a/examples/guardrails/gitlab/skunkworks/provider.tf b/examples/guardrails/gitlab/skunkworks/provider.tf new file mode 100644 index 0000000..0802a09 --- /dev/null +++ b/examples/guardrails/gitlab/skunkworks/provider.tf @@ -0,0 +1,28 @@ +/** + * 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 + * + * 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. + */ + +terraform { + backend "gcs" { + } +} + +provider "google" { + +} + +provider "google-beta" { + +} \ No newline at end of file diff --git a/examples/guardrails/gitlab/skunkworks/variables.tf b/examples/guardrails/gitlab/skunkworks/variables.tf new file mode 100644 index 0000000..6af98ff --- /dev/null +++ b/examples/guardrails/gitlab/skunkworks/variables.tf @@ -0,0 +1,20 @@ +/** + * 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 + * + * 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. + */ + +variable "project" { + type = string + default = "project-id" +} diff --git a/examples/guardrails/jenkins/folder-factory/.github/workflows/terraform-deployment.yml b/examples/guardrails/jenkins/folder-factory/.github/workflows/terraform-deployment.yml new file mode 100644 index 0000000..70f88cb --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/.github/workflows/terraform-deployment.yml @@ -0,0 +1,91 @@ +# 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 +# +# 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. + +name: 'Cloud Deployment' + +on: + push: + branches: + - main + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve + + diff --git a/examples/guardrails/jenkins/folder-factory/.gitignore b/examples/guardrails/jenkins/folder-factory/.gitignore new file mode 100644 index 0000000..a5d8c0e --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.terraform +.terraform.lock.hcl diff --git a/examples/guardrails/jenkins/folder-factory/Jenkinsfile b/examples/guardrails/jenkins/folder-factory/Jenkinsfile new file mode 100644 index 0000000..63cca86 --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/Jenkinsfile @@ -0,0 +1,70 @@ +pipeline { + + agent any + environment { + BUCKET_PATH = credentials('backend-path') + REPO_FULL_NAME = credentials('bucket-repo') + PROJECT_NAME = credentials('project-name') + GCP_PROJECT_NUMBER = credentials('project-id') + workload_identity_pool_id = credentials('wif_pool_id') + workload_identity_pool_provider_id = credentials('wif_pool_provider_id') + SERVICE_ACCOUNT_NAME = credentials('sa-name') + + } + options { + skipDefaultCheckout(true) + } + stages { + stage('clean workspace') { + steps { + cleanWs() + } + } + stage('WIF') { + steps { + withCredentials([file(variable: 'ID_TOKEN_FILE', credentialsId: 'gcp')]) { + writeFile file: "$WORKSPACE_TMP/creds.json", text: """ + { + "type": "external_account", + "audience": "//iam.googleapis.com/projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/${workload_identity_pool_id}/providers/${workload_identity_pool_provider_id}", + "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", + "token_url": "https://sts.googleapis.com/v1/token", + "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com:generateAccessToken", + "credential_source": { + "file": "$ID_TOKEN_FILE", + "format": { + "type": "text" + } + } + } + """ + sh ''' + gcloud auth login --brief --cred-file=$WORKSPACE_TMP/creds.json + ''' +} + + } + } + stage('checkout') { + steps { + checkout scm + } + } + stage('Terraform') { + steps { + + sh ''' + cd guardrails/folder-factory + terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${REPO_FULL_NAME}" + terraform plan + terraform apply -auto-approve + + ''' + + } + } + + + } +} + diff --git a/examples/guardrails/jenkins/folder-factory/README.md b/examples/guardrails/jenkins/folder-factory/README.md new file mode 100644 index 0000000..a139948 --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/README.md @@ -0,0 +1,59 @@ +# Folder Factory + +This is a template for a DevOps folder factory. + +It can be used with [https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory](https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory) and is intended to house the folder configurations: + +![Screenshot 2022-05-10 12 00 19 PM](https://user-images.githubusercontent.com/94000358/169809437-aaa8538e-3ffc-48b3-9028-84e4995de150.png) + +Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: + +Secret configuration + +or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: +``` +env: + STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' + # The workload identity provider that should be used for this repository. + SERVICE_ACCOUNT: 'XXXX@XXXX' + # The service account that should be used for this repository. +``` + +## Setting up folders + +The folder factory will: +- create a folders with defined organisational policies + +It uses YAML configuration files for every folder with the following sample structure: +``` +parent: folders/XXXXXXXXX +org_policies: + policy_boolean: + constraints/compute.disableGuestAttributesAccess: true + constraints/iam.disableServiceAccountCreation: false + constraints/iam.disableServiceAccountKeyCreation: false + constraints/iam.disableServiceAccountKeyUpload: false + constraints/gcp.disableCloudLogging: false + policy_list: + constraints/compute.vmExternalIpAccess: + inherit_from_parent: null + status: true + suggested_value: null + values: +iam: + roles/resourcemanager.projectCreator: + - serviceAccount:XXXXX@XXXXXX +``` + +Every folder is defined with its own yaml file located in the following [Folder](data/folder). diff --git a/examples/guardrails/jenkins/folder-factory/data/folders/folder.yaml.sample b/examples/guardrails/jenkins/folder-factory/data/folders/folder.yaml.sample new file mode 100644 index 0000000..9ea745d --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/data/folders/folder.yaml.sample @@ -0,0 +1,31 @@ +# 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 +# +# 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. + +parent: folders/01234567890 +org_policies: + policy_boolean: + constraints/compute.disableGuestAttributesAccess: true + constraints/iam.disableServiceAccountCreation: false + constraints/iam.disableServiceAccountKeyCreation: false + constraints/iam.disableServiceAccountKeyUpload: false + constraints/gcp.disableCloudLogging: false + policy_list: + constraints/compute.vmExternalIpAccess: + inherit_from_parent: null + status: true + suggested_value: null + values: +iam: + roles/resourcemanager.projectCreator: + - serviceAccount:service-account@project-xyz.iam.gserviceaccount.com \ No newline at end of file diff --git a/examples/guardrails/jenkins/folder-factory/main.tf b/examples/guardrails/jenkins/folder-factory/main.tf new file mode 100644 index 0000000..d024c17 --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/main.tf @@ -0,0 +1,32 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + folders = { + for f in fileset("./data/folders", "**/*.yaml") : + trimsuffix(f, ".yaml") => yamldecode(file("./data/folders/${f}")) + } +} + +module "folder" { + source = "./modules/folder" + for_each = local.folders + name = each.key + parent = each.value.parent + policy_boolean = try(each.value.org_policies.policy_boolean, {}) + policy_list = try(each.value.org_policies.policy_list, {}) + iam = try(each.value.iam, {}) +} \ No newline at end of file diff --git a/examples/guardrails/jenkins/folder-factory/modules/folder/README.md b/examples/guardrails/jenkins/folder-factory/modules/folder/README.md new file mode 100644 index 0000000..4f6898b --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/modules/folder/README.md @@ -0,0 +1,299 @@ +# Google Cloud Folder Module + +This module allows the creation and management of folders, including support for IAM bindings, organization policies, and hierarchical firewall rules. + +## Examples + +### IAM bindings + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + group_iam = { + "cloud-owners@example.org" = [ + "roles/owner", + "roles/resourcemanager.projectCreator" + ] + } + iam = { + "roles/owner" = ["user:one@example.com"] + } +} +# tftest modules=1 resources=3 +``` + +### Organization policies + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + policy_boolean = { + "constraints/compute.disableGuestAttributesAccess" = true + "constraints/compute.skipDefaultNetworkCreation" = true + } + policy_list = { + "constraints/compute.trustedImageProjects" = { + inherit_from_parent = null + suggested_value = null + status = true + values = ["projects/my-project"] + } + } +} +# tftest modules=1 resources=4 +``` + +### Firewall policy factory + +In the same way as for the [organization](../organization) module, the in-built factory allows you to define a single policy, using one file for rules, and an optional file for CIDR range substitution variables. Remember that non-absolute paths are relative to the root module (the folder where you run `terraform`). + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + firewall_policy_factory = { + cidr_file = "data/cidrs.yaml" + policy_name = null + rules_file = "data/rules.yaml" + } + firewall_policy_association = { + factory-policy = module.folder.firewall_policy_id["factory"] + } +} +# tftest skip +``` + +```yaml +# cidrs.yaml + +rfc1918: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 +``` + +```yaml +# rules.yaml + +allow-admins: + description: Access from the admin subnet to all subnets + direction: INGRESS + action: allow + priority: 1000 + ranges: + - $rfc1918 + ports: + all: [] + target_resources: null + enable_logging: false + +allow-ssh-from-iap: + description: Enable SSH from IAP + direction: INGRESS + action: allow + priority: 1002 + ranges: + - 35.235.240.0/20 + ports: + tcp: ["22"] + target_resources: null + enable_logging: false +``` + +### Logging Sinks + +```hcl +module "gcs" { + source = "./modules/gcs" + project_id = "my-project" + name = "gcs_sink" + force_destroy = true +} + +module "dataset" { + source = "./modules/bigquery-dataset" + project_id = "my-project" + id = "bq_sink" +} + +module "pubsub" { + source = "./modules/pubsub" + project_id = "my-project" + name = "pubsub_sink" +} + +module "bucket" { + source = "./modules/logging-bucket" + parent_type = "project" + parent = "my-project" + id = "bucket" +} + +module "folder-sink" { + source = "./modules/folder" + parent = "folders/657104291943" + name = "my-folder" + logging_sinks = { + warnings = { + type = "storage" + destination = module.gcs.name + filter = "severity=WARNING" + include_children = true + exclusions = {} + } + info = { + type = "bigquery" + destination = module.dataset.id + filter = "severity=INFO" + include_children = true + exclusions = {} + } + notice = { + type = "pubsub" + destination = module.pubsub.id + filter = "severity=NOTICE" + include_children = true + exclusions = {} + } + debug = { + type = "logging" + destination = module.bucket.id + filter = "severity=DEBUG" + include_children = true + exclusions = { + no-compute = "logName:compute" + } + } + } + logging_exclusions = { + no-gce-instances = "resource.type=gce_instance" + } +} +# tftest modules=5 resources=14 +``` + +### Hierarchical firewall policies + +```hcl +module "folder1" { + source = "./modules/folder" + parent = var.organization_id + name = "policy-container" + + firewall_policies = { + iap-policy = { + allow-iap-ssh = { + description = "Always allow ssh from IAP" + direction = "INGRESS" + action = "allow" + priority = 100 + ranges = ["35.235.240.0/20"] + ports = { tcp = ["22"] } + target_service_accounts = null + target_resources = null + logging = false + } + } + } + firewall_policy_association = { + iap-policy = "iap-policy" + } +} + +module "folder2" { + source = "./modules/folder" + parent = var.organization_id + name = "hf2" + firewall_policy_association = { + iap-policy = module.folder1.firewall_policy_id["iap-policy"] + } +} +# tftest modules=2 resources=6 +``` + +## 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. + +```hcl +module "org" { + source = "./modules/organization" + organization_id = var.organization_id + tags = { + environment = { + description = "Environment specification." + iam = null + values = { + dev = null + prod = null + } + } + } +} + +module "folder" { + source = "./modules/folder" + name = "Test" + parent = module.org.organization_id + tag_bindings = { + env-prod = module.org.tag_values["environment/prod"].id + foo = "tagValues/12345678" + } +} +# tftest modules=2 resources=6 +``` + + + + +## Files + +| name | description | resources | +|---|---|---| +| [firewall-policies.tf](./firewall-policies.tf) | None | google_compute_firewall_policy · google_compute_firewall_policy_association · google_compute_firewall_policy_rule | +| [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_folder_iam_binding | +| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_folder_exclusion · google_logging_folder_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | +| [main.tf](./main.tf) | Module-level locals and resources. | google_essential_contacts_contact · google_folder | +| [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_folder_organization_policy | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding | +| [variables.tf](./variables.tf) | Module variables. | | +| [versions.tf](./versions.tf) | Version pins. | | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | +| [firewall_policies](variables.tf#L24) | Hierarchical firewall policies created in this folder. | map(map(object({…}))) | | {} | +| [firewall_policy_association](variables.tf#L41) | The hierarchical firewall policy to associate to this folder. Must be either a key in the `firewall_policies` map or the id of a policy defined somewhere else. | map(string) | | {} | +| [firewall_policy_factory](variables.tf#L48) | Configuration for the firewall policy factory. | object({…}) | | null | +| [folder_create](variables.tf#L58) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | +| [group_iam](variables.tf#L64) | 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. | map(list(string)) | | {} | +| [iam](variables.tf#L71) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [id](variables.tf#L78) | Folder ID in case you use folder_create=false. | string | | null | +| [logging_exclusions](variables.tf#L84) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L91) | Logging sinks to create for this folder. | map(object({…})) | | {} | +| [name](variables.tf#L112) | Folder name. | string | | null | +| [parent](variables.tf#L118) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [policy_boolean](variables.tf#L128) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | +| [policy_list](variables.tf#L135) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L147) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [firewall_policies](outputs.tf#L16) | Map of firewall policy resources created in this folder. | | +| [firewall_policy_id](outputs.tf#L21) | Map of firewall policy ids created in this folder. | | +| [folder](outputs.tf#L26) | Folder resource. | | +| [id](outputs.tf#L31) | Folder id. | | +| [name](outputs.tf#L41) | Folder name. | | +| [sink_writer_identities](outputs.tf#L46) | Writer identities created for each sink. | | + + diff --git a/examples/guardrails/jenkins/folder-factory/modules/folder/firewall-policies.tf b/examples/guardrails/jenkins/folder-factory/modules/folder/firewall-policies.tf new file mode 100644 index 0000000..96224c5 --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/modules/folder/firewall-policies.tf @@ -0,0 +1,93 @@ +/** + * 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 + * + * 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 { + _factory_cidrs = try( + yamldecode(file(var.firewall_policy_factory.cidr_file)), {} + ) + _factory_name = ( + try(var.firewall_policy_factory.policy_name, null) == null + ? "factory" + : var.firewall_policy_factory.policy_name + ) + _factory_rules = try( + yamldecode(file(var.firewall_policy_factory.rules_file)), {} + ) + _factory_rules_parsed = { + for name, rule in local._factory_rules : name => merge(rule, { + ranges = flatten([ + for r in(rule.ranges == null ? [] : rule.ranges) : + lookup(local._factory_cidrs, trimprefix(r, "$"), r) + ]) + }) + } + _merged_rules = flatten([ + for policy, rules in local.firewall_policies : [ + for name, rule in rules : merge(rule, { + policy = policy + name = name + }) + ] + ]) + firewall_policies = merge(var.firewall_policies, ( + length(local._factory_rules) == 0 + ? {} + : { (local._factory_name) = local._factory_rules_parsed } + )) + firewall_rules = { + for r in local._merged_rules : "${r.policy}-${r.name}" => r + } +} + +resource "google_compute_firewall_policy" "policy" { + for_each = local.firewall_policies + short_name = each.key + parent = local.folder.id +} + +resource "google_compute_firewall_policy_rule" "rule" { + for_each = local.firewall_rules + firewall_policy = google_compute_firewall_policy.policy[each.value.policy].id + action = each.value.action + direction = each.value.direction + priority = try(each.value.priority, null) + target_resources = try(each.value.target_resources, null) + target_service_accounts = try(each.value.target_service_accounts, null) + enable_logging = try(each.value.logging, null) + # preview = each.value.preview + description = each.value.description + match { + src_ip_ranges = each.value.direction == "INGRESS" ? each.value.ranges : null + dest_ip_ranges = each.value.direction == "EGRESS" ? each.value.ranges : null + dynamic "layer4_configs" { + for_each = each.value.ports + iterator = port + content { + ip_protocol = port.key + ports = port.value + } + } + } +} + + +resource "google_compute_firewall_policy_association" "association" { + for_each = var.firewall_policy_association + name = replace(local.folder.id, "/", "-") + attachment_target = local.folder.id + firewall_policy = try(google_compute_firewall_policy.policy[each.value].id, each.value) +} + diff --git a/examples/guardrails/jenkins/folder-factory/modules/folder/iam.tf b/examples/guardrails/jenkins/folder-factory/modules/folder/iam.tf new file mode 100644 index 0000000..52886ba --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/modules/folder/iam.tf @@ -0,0 +1,40 @@ +/** + * 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 + * + * 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 IAM bindings, roles and audit logging resources. + +locals { + group_iam_roles = distinct(flatten(values(var.group_iam))) + group_iam = { + for r in local.group_iam_roles : r => [ + for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null + ] + } + iam = { + for role in distinct(concat(keys(var.iam), keys(local.group_iam))) : + role => concat( + try(var.iam[role], []), + try(local.group_iam[role], []) + ) + } +} + +resource "google_folder_iam_binding" "authoritative" { + for_each = local.iam + folder = local.folder.name + role = each.key + members = each.value +} diff --git a/examples/guardrails/jenkins/folder-factory/modules/folder/logging.tf b/examples/guardrails/jenkins/folder-factory/modules/folder/logging.tf new file mode 100644 index 0000000..d6a195e --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/modules/folder/logging.tf @@ -0,0 +1,91 @@ +/** + * 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 + * + * 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 Log sinks and supporting resources. + +locals { + sink_bindings = { + for type in ["bigquery", "pubsub", "logging", "storage"] : + type => { + for name, sink in var.logging_sinks : + name => sink + if sink.type == type + } + } +} + +resource "google_logging_folder_sink" "sink" { + for_each = var.logging_sinks + name = each.key + #description = "${each.key} (Terraform-managed)." + folder = local.folder.name + destination = "${each.value.type}.googleapis.com/${each.value.destination}" + filter = each.value.filter + include_children = each.value.include_children + + dynamic "exclusions" { + for_each = each.value.exclusions + iterator = exclusion + content { + name = exclusion.key + filter = exclusion.value + } + } + + depends_on = [ + google_folder_iam_binding.authoritative + ] +} + +resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { + for_each = local.sink_bindings["storage"] + bucket = each.value.destination + role = "roles/storage.objectCreator" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" { + for_each = local.sink_bindings["bigquery"] + project = split("/", each.value.destination)[1] + dataset_id = split("/", each.value.destination)[3] + role = "roles/bigquery.dataEditor" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" { + for_each = local.sink_bindings["pubsub"] + project = split("/", each.value.destination)[1] + topic = split("/", each.value.destination)[3] + role = "roles/pubsub.publisher" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_project_iam_member" "bucket-sinks-binding" { + for_each = local.sink_bindings["logging"] + project = split("/", each.value.destination)[1] + role = "roles/logging.bucketWriter" + member = google_logging_folder_sink.sink[each.key].writer_identity + # TODO(jccb): use a condition to limit writer-identity only to this + # bucket +} + +resource "google_logging_folder_exclusion" "logging-exclusion" { + for_each = var.logging_exclusions + name = each.key + folder = local.folder.name + description = "${each.key} (Terraform-managed)." + filter = each.value +} diff --git a/examples/guardrails/jenkins/folder-factory/modules/folder/main.tf b/examples/guardrails/jenkins/folder-factory/modules/folder/main.tf new file mode 100644 index 0000000..5d285d2 --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/modules/folder/main.tf @@ -0,0 +1,43 @@ +/** + * 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 + * + * 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 { + folder = ( + var.folder_create + ? try(google_folder.folder.0, null) + : try(data.google_folder.folder.0, null) + ) +} + +data "google_folder" "folder" { + count = var.folder_create ? 0 : 1 + folder = var.id +} + +resource "google_folder" "folder" { + count = var.folder_create ? 1 : 0 + display_name = var.name + parent = var.parent +} + +resource "google_essential_contacts_contact" "contact" { + provider = google-beta + for_each = var.contacts + parent = local.folder.name + email = each.key + language_tag = "en" + notification_category_subscriptions = each.value +} diff --git a/examples/guardrails/jenkins/folder-factory/modules/folder/organization-policies.tf b/examples/guardrails/jenkins/folder-factory/modules/folder/organization-policies.tf new file mode 100644 index 0000000..177a3d8 --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/modules/folder/organization-policies.tf @@ -0,0 +1,90 @@ +/** + * 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 + * + * 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 Folder-level organization policies. + +resource "google_folder_organization_policy" "boolean" { + for_each = var.policy_boolean + folder = local.folder.name + constraint = each.key + + dynamic "boolean_policy" { + for_each = each.value == null ? [] : [each.value] + iterator = policy + content { + enforced = policy.value + } + } + + dynamic "restore_policy" { + for_each = each.value == null ? [""] : [] + content { + default = true + } + } +} + +resource "google_folder_organization_policy" "list" { + for_each = var.policy_list + folder = local.folder.name + constraint = each.key + + dynamic "list_policy" { + for_each = each.value.status == null ? [] : [each.value] + iterator = policy + content { + inherit_from_parent = policy.value.inherit_from_parent + suggested_value = policy.value.suggested_value + dynamic "allow" { + for_each = policy.value.status ? [""] : [] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + dynamic "deny" { + for_each = policy.value.status ? [] : [""] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + } + } + + dynamic "restore_policy" { + for_each = each.value.status == null ? [true] : [] + content { + default = true + } + } +} diff --git a/examples/guardrails/jenkins/folder-factory/modules/folder/outputs.tf b/examples/guardrails/jenkins/folder-factory/modules/folder/outputs.tf new file mode 100644 index 0000000..37babc6 --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/modules/folder/outputs.tf @@ -0,0 +1,51 @@ +/** + * 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 + * + * 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. + */ +output "firewall_policies" { + description = "Map of firewall policy resources created in this folder." + value = { for k, v in google_compute_firewall_policy.policy : k => v } +} + +output "firewall_policy_id" { + description = "Map of firewall policy ids created in this folder." + value = { for k, v in google_compute_firewall_policy.policy : k => v.id } +} + +output "folder" { + description = "Folder resource." + value = local.folder +} + +output "id" { + description = "Folder id." + value = local.folder.name + depends_on = [ + google_folder_iam_binding.authoritative, + google_folder_organization_policy.boolean, + google_folder_organization_policy.list + ] +} + +output "name" { + description = "Folder name." + value = local.folder.display_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 + } +} diff --git a/examples/guardrails/jenkins/folder-factory/modules/folder/tags.tf b/examples/guardrails/jenkins/folder-factory/modules/folder/tags.tf new file mode 100644 index 0000000..2cd2f2f --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/modules/folder/tags.tf @@ -0,0 +1,21 @@ +/** + * 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 + * + * 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. + */ + +resource "google_tags_tag_binding" "binding" { + for_each = coalesce(var.tag_bindings, {}) + parent = "//cloudresourcemanager.googleapis.com/${local.folder.id}" + tag_value = each.value +} diff --git a/examples/guardrails/jenkins/folder-factory/modules/folder/variables.tf b/examples/guardrails/jenkins/folder-factory/modules/folder/variables.tf new file mode 100644 index 0000000..a3f32e3 --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/modules/folder/variables.tf @@ -0,0 +1,151 @@ +/** + * 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 + * + * 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. + */ + +variable "contacts" { + description = "List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES." + type = map(list(string)) + default = {} + nullable = false +} + +variable "firewall_policies" { + description = "Hierarchical firewall policies created in this folder." + type = map(map(object({ + action = string + description = string + direction = string + logging = bool + ports = map(list(string)) + priority = number + ranges = list(string) + target_resources = list(string) + target_service_accounts = list(string) + }))) + default = {} + nullable = false +} + +variable "firewall_policy_association" { + description = "The hierarchical firewall policy to associate to this folder. Must be either a key in the `firewall_policies` map or the id of a policy defined somewhere else." + type = map(string) + default = {} + nullable = false +} + +variable "firewall_policy_factory" { + description = "Configuration for the firewall policy factory." + type = object({ + cidr_file = string + policy_name = string + rules_file = string + }) + default = null +} + +variable "folder_create" { + description = "Create folder. When set to false, uses id to reference an existing folder." + type = bool + default = true +} + +variable "group_iam" { + description = "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." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam" { + description = "IAM bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "id" { + description = "Folder ID in case you use folder_create=false." + type = string + default = null +} + +variable "logging_exclusions" { + description = "Logging exclusions for this folder in the form {NAME -> FILTER}." + type = map(string) + default = {} + nullable = false +} + +variable "logging_sinks" { + description = "Logging sinks to create for this folder." + type = map(object({ + destination = string + type = string + filter = string + include_children = bool + # TODO exclusions also support description and disabled + exclusions = map(string) + })) + validation { + condition = alltrue([ + for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } + default = {} + nullable = false +} + +variable "name" { + description = "Folder name." + type = string + default = null +} + +variable "parent" { + description = "Parent in folders/folder_id or organizations/org_id format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "policy_boolean" { + description = "Map of boolean org policies and enforcement value, set value to null for policy restore." + type = map(bool) + default = {} + nullable = false +} + +variable "policy_list" { + description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." + type = map(object({ + inherit_from_parent = bool + suggested_value = string + status = bool + values = list(string) + })) + default = {} + nullable = false +} + +variable "tag_bindings" { + description = "Tag bindings for this folder, in key => tag value id format." + type = map(string) + default = null +} diff --git a/examples/guardrails/jenkins/folder-factory/modules/folder/versions.tf b/examples/guardrails/jenkins/folder-factory/modules/folder/versions.tf new file mode 100644 index 0000000..e72a780 --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/modules/folder/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.1.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/jenkins/folder-factory/outputs.tf b/examples/guardrails/jenkins/folder-factory/outputs.tf new file mode 100644 index 0000000..a84b9d5 --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/outputs.tf @@ -0,0 +1,20 @@ +/** + * 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 + * + * 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. + */ + +output "folders" { + description = "Created folders." + value = module.folder +} \ No newline at end of file diff --git a/examples/guardrails/jenkins/folder-factory/provider.tf b/examples/guardrails/jenkins/folder-factory/provider.tf new file mode 100644 index 0000000..0e17ef9 --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/provider.tf @@ -0,0 +1,28 @@ +/** + * 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 + * + * 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. + */ + +terraform { + backend "gcs" { + } +} + +provider "google" { + +} + +provider "google-beta" { + +} diff --git a/examples/guardrails/jenkins/folder-factory/variables.tf b/examples/guardrails/jenkins/folder-factory/variables.tf new file mode 100644 index 0000000..156a5ac --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/variables.tf @@ -0,0 +1,15 @@ +/** + * 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 + * + * 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. + */ \ No newline at end of file diff --git a/examples/guardrails/jenkins/project-factory/.github/workflows/terraform-deployment.yml b/examples/guardrails/jenkins/project-factory/.github/workflows/terraform-deployment.yml new file mode 100644 index 0000000..4099678 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/.github/workflows/terraform-deployment.yml @@ -0,0 +1,94 @@ +# 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 +# +# 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. + +name: 'Cloud Deployment' + +on: + push: + branches: + - main + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + FOLDER: ${{ secrets.FOLDER }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} + +# OR SET MANUALLY +# +#env: +# STATE_BUCKET: 'XXXX' +# FOLDER: 'folders/XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan -var "folder=$FOLDER" + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -var "folder=$FOLDER" -auto-approve + + diff --git a/examples/guardrails/jenkins/project-factory/.gitignore b/examples/guardrails/jenkins/project-factory/.gitignore new file mode 100644 index 0000000..a5d8c0e --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.terraform +.terraform.lock.hcl diff --git a/examples/guardrails/jenkins/project-factory/Jenkinsfile b/examples/guardrails/jenkins/project-factory/Jenkinsfile new file mode 100644 index 0000000..a7f9447 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/Jenkinsfile @@ -0,0 +1,39 @@ +pipeline { + + agent any + environment { + BUCKET_PATH = credentials('backend-path') + + + } + + options { + skipDefaultCheckout(true) + } + stages { + stage('clean workspace') { + steps { + cleanWs() + } + } + stage('checkout') { + steps { + checkout scm + } + } + + stage('Terraform') { + steps { + + sh ''' + cd guardrails/project-factory + terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=project-factory-staging-tfstate" -var-file="staging.tfvars" + terraform plan -var-file="staging.tfvars" + terraform apply -var-file="staging.tfvars" -auto-approve + + ''' + + } + } + } +} diff --git a/examples/guardrails/jenkins/project-factory/README.md b/examples/guardrails/jenkins/project-factory/README.md new file mode 100644 index 0000000..f901e7f --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/README.md @@ -0,0 +1,75 @@ +# Project Factory + +This is a template for a DevOps project factory. + +It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory) and is intended to house the projects of a specified folder: + +Overview + +Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. + +![Folder Factory](https://user-images.githubusercontent.com/94000358/169809882-f5ff9fb1-d037-49de-8c2c-bf0d457b662f.png) + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: + +Secret settings + +or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: +``` +env: + STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + FOLDER: 'folders/XXXX' + # The folder under which the projects should be created + WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' + # The workload identity provider that should be used for this repository. + SERVICE_ACCOUNT: 'XXXX@XXXX' + # The service account that should be used for this repository. +``` + +## Setting up projects + +The project factory will: +- create a service account with defined rights +- create a project within the folder +- connect the service account to the Github repository informantion + +It uses YAML configuration files for every project with the following sample structure: +``` +billing_account_id: XXXXXX-XXXXXX-XXXXXX +roles: + - roles/viewer + - roles/iam.serviceAccountUser + - roles/iam.securityReviewer + - roles/monitoring.viewer + - roles/monitoring.editor + - roles/monitoring.alertPolicyViewer + - roles/monitoring.alertPolicyEditor + - roles/monitoring.dashboardViewer + - roles/monitoring.dashboardEditor + - roles/monitoring.notificationChannelViewer + - roles/monitoring.notificationChannelEditor + - roles/monitoring.servicesViewer + - roles/monitoring.servicesEditor + - roles/monitoring.uptimeCheckConfigViewer + - roles/monitoring.uptimeCheckConfigEditor + - roles/secretmanager.viewer + - roles/secretmanager.secretVersionManager + - roles/secretmanager.admin + - roles/storage.admin + - roles/storage.objectAdmin + - roles/storage.objectCreator + - roles/storage.objectViewer +repo_provider: github +repo_name: devops-governance/skunkworks +repo_branch: dev +``` + +Every project is defined with its own file located in the [Project Folder](data/projects). diff --git a/examples/guardrails/jenkins/project-factory/data/projects/project.yaml.sample b/examples/guardrails/jenkins/project-factory/data/projects/project.yaml.sample new file mode 100644 index 0000000..d813f4b --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/data/projects/project.yaml.sample @@ -0,0 +1,41 @@ +# 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 +# +# 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. + +billing_account_id: XXXXXX-XXXXXX-XXXXXX +roles: + - roles/viewer + - roles/iam.serviceAccountUser + - roles/iam.securityReviewer + - roles/monitoring.viewer + - roles/monitoring.editor + - roles/monitoring.alertPolicyViewer + - roles/monitoring.alertPolicyEditor + - roles/monitoring.dashboardViewer + - roles/monitoring.dashboardEditor + - roles/monitoring.notificationChannelViewer + - roles/monitoring.notificationChannelEditor + - roles/monitoring.servicesViewer + - roles/monitoring.servicesEditor + - roles/monitoring.uptimeCheckConfigViewer + - roles/monitoring.uptimeCheckConfigEditor + - roles/secretmanager.viewer + - roles/secretmanager.secretVersionManager + - roles/secretmanager.admin + - roles/storage.admin + - roles/storage.objectAdmin + - roles/storage.objectCreator + - roles/storage.objectViewer +repo_provider: github +repo_name: github-org/github-repo +repo_branch: dev diff --git a/examples/guardrails/jenkins/project-factory/main.tf b/examples/guardrails/jenkins/project-factory/main.tf new file mode 100644 index 0000000..6c7d4d7 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/main.tf @@ -0,0 +1,36 @@ +/** + * 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 + * + * 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 { + projects = { + for f in fileset("./data/projects", "**/*.yaml") : + trimsuffix(f, ".yaml") => yamldecode(file("./data/projects/${f}")) + } +} + +module "project" { + source = "./modules/project_plus" + for_each = local.projects + team = each.key + repo_sub = each.value.repo_branch + repo_provider = each.value.repo_provider + billing_account = each.value.billing_account_id + folder = var.folder + roles = try(each.value.roles, []) + wif-pool = google_iam_workload_identity_pool.wif-pool-jenkins.name + depends_on = [google_iam_workload_identity_pool.wif-pool-jenkins] +} + diff --git a/examples/guardrails/jenkins/project-factory/modules/project/README.md b/examples/guardrails/jenkins/project-factory/modules/project/README.md new file mode 100644 index 0000000..e4f2139 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project/README.md @@ -0,0 +1,308 @@ +# Project Module + +## Examples + +### Minimal example with IAM + +```hcl +locals { + gke_service_account = "my_gke_service_account" +} + +module "project" { + source = "./modules/project" + billing_account = "123456-123456-123456" + name = "project-example" + parent = "folders/1234567890" + prefix = "foo" + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + iam = { + "roles/container.hostServiceAgentUser" = [ + "serviceAccount:${local.gke_service_account}" + ] + } +} +# tftest modules=1 resources=4 +``` + +### Minimal example with IAM additive roles + +```hcl +module "project" { + source = "./modules/project" + name = "project-example" + + iam_additive = { + "roles/viewer" = [ + "group:one@example.org", "group:two@xample.org" + ], + "roles/storage.objectAdmin" = [ + "group:two@example.org" + ], + "roles/owner" = [ + "group:three@example.org" + ], + } +} +# tftest modules=1 resources=5 +``` + +### Shared VPC service + +```hcl +module "project" { + source = "./modules/project" + name = "project-example" + + shared_vpc_service_config = { + attach = true + host_project = "my-host-project" + service_identity_iam = { + "roles/compute.networkUser" = [ + "cloudservices", "container-engine" + ] + "roles/vpcaccess.user" = [ + "cloudrun" + ] + "roles/container.hostServiceAgentUser" = [ + "container-engine" + ] + } + } +} +# tftest modules=1 resources=6 +``` + +### Organization policies + +```hcl +module "project" { + source = "./modules/project" + billing_account = "123456-123456-123456" + name = "project-example" + parent = "folders/1234567890" + prefix = "foo" + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + policy_boolean = { + "constraints/compute.disableGuestAttributesAccess" = true + "constraints/compute.skipDefaultNetworkCreation" = true + } + policy_list = { + "constraints/compute.trustedImageProjects" = { + inherit_from_parent = null + suggested_value = null + status = true + values = ["projects/my-project"] + } + } +} +# tftest modules=1 resources=6 +``` + +## Logging Sinks + +```hcl +module "gcs" { + source = "./modules/gcs" + project_id = var.project_id + name = "gcs_sink" + force_destroy = true +} + +module "dataset" { + source = "./modules/bigquery-dataset" + project_id = var.project_id + id = "bq_sink" +} + +module "pubsub" { + source = "./modules/pubsub" + project_id = var.project_id + name = "pubsub_sink" +} + +module "bucket" { + source = "./modules/logging-bucket" + parent_type = "project" + parent = "my-project" + id = "bucket" +} + +module "project-host" { + source = "./modules/project" + name = "my-project" + billing_account = "123456-123456-123456" + parent = "folders/1234567890" + logging_sinks = { + warnings = { + type = "storage" + destination = module.gcs.name + filter = "severity=WARNING" + iam = false + unique_writer = false + exclusions = {} + } + info = { + type = "bigquery" + destination = module.dataset.id + filter = "severity=INFO" + iam = false + unique_writer = false + exclusions = {} + } + notice = { + type = "pubsub" + destination = module.pubsub.id + filter = "severity=NOTICE" + iam = true + unique_writer = false + exclusions = {} + } + debug = { + type = "logging" + destination = module.bucket.id + filter = "severity=DEBUG" + iam = true + unique_writer = false + exclusions = { + no-compute = "logName:compute" + } + } + } + logging_exclusions = { + no-gce-instances = "resource.type=gce_instance" + } +} +# tftest modules=5 resources=12 +``` + +## Cloud KMS encryption keys + +```hcl +module "project" { + source = "./modules/project" + name = "my-project" + billing_account = "123456-123456-123456" + prefix = "foo" + services = [ + "compute.googleapis.com", + "storage.googleapis.com" + ] + service_encryption_key_ids = { + compute = [ + "projects/kms-central-prj/locations/europe-west3/keyRings/my-keyring/cryptoKeys/europe3-gce", + "projects/kms-central-prj/locations/europe-west4/keyRings/my-keyring/cryptoKeys/europe4-gce" + ] + storage = [ + "projects/kms-central-prj/locations/europe/keyRings/my-keyring/cryptoKeys/europe-gcs" + ] + } +} +# tftest modules=1 resources=7 +``` + +## 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. + +```hcl +module "org" { + source = "./modules/organization" + organization_id = var.organization_id + tags = { + environment = { + description = "Environment specification." + iam = null + values = { + dev = null + prod = null + } + } + } +} + +module "project" { + source = "./modules/project" + name = "test-project" + tag_bindings = { + env-prod = module.org.tag_values["environment/prod"].id + foo = "tagValues/12345678" + } +} +# tftest modules=2 resources=6 +``` + + + + +## Files + +| name | description | resources | +|---|---|---| +| [iam.tf](./iam.tf) | Generic and OSLogin-specific IAM bindings and roles. | google_project_iam_binding · google_project_iam_custom_role · google_project_iam_member | +| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_project_exclusion · google_logging_project_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | +| [main.tf](./main.tf) | Module-level locals and resources. | google_compute_project_metadata_item · google_essential_contacts_contact · google_monitoring_monitored_project · google_project · google_project_service · google_resource_manager_lien | +| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_project_organization_policy | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [service-accounts.tf](./service-accounts.tf) | Service identities and supporting resources. | google_kms_crypto_key_iam_member · google_project_service_identity | +| [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project · google_compute_shared_vpc_service_project · google_project_iam_member | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding | +| [variables.tf](./variables.tf) | Module variables. | | +| [versions.tf](./versions.tf) | Version pins. | | +| [vpc-sc.tf](./vpc-sc.tf) | VPC-SC project-level perimeter configuration. | google_access_context_manager_service_perimeter_resource | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [name](variables.tf#L125) | Project name and id suffix. | string | ✓ | | +| [auto_create_network](variables.tf#L17) | Whether to create the default network for the project. | bool | | false | +| [billing_account](variables.tf#L23) | Billing account id. | string | | null | +| [contacts](variables.tf#L29) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | +| [custom_roles](variables.tf#L36) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| [descriptive_name](variables.tf#L43) | Name of the project name. Used for project name instead of `name` variable. | string | | null | +| [group_iam](variables.tf#L49) | 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. | map(list(string)) | | {} | +| [iam](variables.tf#L56) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive](variables.tf#L63) | IAM additive bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive_members](variables.tf#L70) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | map(list(string)) | | {} | +| [labels](variables.tf#L76) | Resource labels. | map(string) | | {} | +| [lien_reason](variables.tf#L83) | If non-empty, creates a project lien with this description. | string | | "" | +| [logging_exclusions](variables.tf#L89) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L96) | Logging sinks to create for this project. | map(object({…})) | | {} | +| [metric_scopes](variables.tf#L118) | List of projects that will act as metric scopes for this project. | list(string) | | [] | +| [oslogin](variables.tf#L130) | Enable OS Login. | bool | | false | +| [oslogin_admins](variables.tf#L136) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | +| [oslogin_users](variables.tf#L144) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | +| [parent](variables.tf#L151) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [policy_boolean](variables.tf#L161) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | +| [policy_list](variables.tf#L168) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | +| [prefix](variables.tf#L180) | Prefix used to generate project id and name. | string | | null | +| [project_create](variables.tf#L186) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | +| [service_config](variables.tf#L192) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L204) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | +| [service_perimeter_bridges](variables.tf#L211) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | +| [service_perimeter_standard](variables.tf#L218) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | +| [services](variables.tf#L224) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L230) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L239) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | +| [skip_delete](variables.tf#L249) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [tag_bindings](variables.tf#L255) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [custom_roles](outputs.tf#L17) | Ids of the created custom roles. | | +| [name](outputs.tf#L25) | Project name. | | +| [number](outputs.tf#L38) | Project number. | | +| [project_id](outputs.tf#L51) | Project id. | | +| [service_accounts](outputs.tf#L66) | Product robot service accounts in project. | | +| [sink_writer_identities](outputs.tf#L82) | Writer identities created for each sink. | | + + diff --git a/examples/guardrails/jenkins/project-factory/modules/project/iam.tf b/examples/guardrails/jenkins/project-factory/modules/project/iam.tf new file mode 100644 index 0000000..69925cc --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project/iam.tf @@ -0,0 +1,115 @@ +/** + * 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 + * + * 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 Generic and OSLogin-specific IAM bindings and roles. + +# IAM notes: +# - external users need to have accepted the invitation email to join +# - oslogin roles also require role to list instances +# - additive (non-authoritative) roles might fail due to dynamic values + +locals { + _group_iam_roles = distinct(flatten(values(var.group_iam))) + _group_iam = { + for r in local._group_iam_roles : r => [ + 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( + try(var.iam[role], []), + 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_project_iam_custom_role" "roles" { + for_each = var.custom_roles + project = local.project.project_id + role_id = each.key + title = "Custom role ${each.key}" + description = "Terraform-managed." + permissions = each.value +} + +resource "google_project_iam_binding" "authoritative" { + for_each = local.iam + project = local.project.project_id + role = each.key + members = each.value + depends_on = [ + google_project_service.project_services, + google_project_iam_custom_role.roles + ] +} + +resource "google_project_iam_member" "additive" { + for_each = ( + length(var.iam_additive) + length(var.iam_additive_members) > 0 + ? local.iam_additive + : {} + ) + project = local.project.project_id + role = each.value.role + member = each.value.member + depends_on = [ + google_project_service.project_services, + google_project_iam_custom_role.roles + ] +} + +resource "google_project_iam_member" "oslogin_iam_serviceaccountuser" { + for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([]) + project = local.project.project_id + role = "roles/iam.serviceAccountUser" + member = each.value +} + +resource "google_project_iam_member" "oslogin_compute_viewer" { + for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([]) + project = local.project.project_id + role = "roles/compute.viewer" + member = each.value +} + +resource "google_project_iam_member" "oslogin_admins" { + for_each = var.oslogin ? toset(var.oslogin_admins) : toset([]) + project = local.project.project_id + role = "roles/compute.osAdminLogin" + member = each.value +} + +resource "google_project_iam_member" "oslogin_users" { + for_each = var.oslogin ? toset(var.oslogin_users) : toset([]) + project = local.project.project_id + role = "roles/compute.osLogin" + member = each.value +} diff --git a/examples/guardrails/jenkins/project-factory/modules/project/logging.tf b/examples/guardrails/jenkins/project-factory/modules/project/logging.tf new file mode 100644 index 0000000..04d7abf --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project/logging.tf @@ -0,0 +1,91 @@ +/** + * 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 + * + * 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 Log sinks and supporting resources. + +locals { + sink_bindings = { + for type in ["bigquery", "pubsub", "logging", "storage"] : + type => { + for name, sink in var.logging_sinks : + name => sink if sink.iam && sink.type == type + } + } +} + +resource "google_logging_project_sink" "sink" { + for_each = var.logging_sinks + name = each.key + #description = "${each.key} (Terraform-managed)." + project = local.project.project_id + destination = "${each.value.type}.googleapis.com/${each.value.destination}" + filter = each.value.filter + unique_writer_identity = each.value.unique_writer + + dynamic "exclusions" { + for_each = each.value.exclusions + iterator = exclusion + content { + name = exclusion.key + filter = exclusion.value + } + } + + depends_on = [ + google_project_iam_binding.authoritative, + google_project_iam_member.additive + ] +} + +resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { + for_each = local.sink_bindings["storage"] + bucket = each.value.destination + role = "roles/storage.objectCreator" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" { + for_each = local.sink_bindings["bigquery"] + project = split("/", each.value.destination)[1] + dataset_id = split("/", each.value.destination)[3] + role = "roles/bigquery.dataEditor" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" { + for_each = local.sink_bindings["pubsub"] + project = split("/", each.value.destination)[1] + topic = split("/", each.value.destination)[3] + role = "roles/pubsub.publisher" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_project_iam_member" "bucket-sinks-binding" { + for_each = local.sink_bindings["logging"] + project = split("/", each.value.destination)[1] + role = "roles/logging.bucketWriter" + member = google_logging_project_sink.sink[each.key].writer_identity + # TODO(jccb): use a condition to limit writer-identity only to this + # bucket +} + +resource "google_logging_project_exclusion" "logging-exclusion" { + for_each = var.logging_exclusions + name = each.key + project = local.project.project_id + description = "${each.key} (Terraform-managed)." + filter = each.value +} diff --git a/examples/guardrails/jenkins/project-factory/modules/project/main.tf b/examples/guardrails/jenkins/project-factory/modules/project/main.tf new file mode 100644 index 0000000..9f0dff4 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project/main.tf @@ -0,0 +1,95 @@ +/** + * 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 + * + * 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 { + descriptive_name = ( + var.descriptive_name != null ? var.descriptive_name : "${local.prefix}${var.name}" + ) + parent_type = var.parent == null ? null : split("/", var.parent)[0] + parent_id = var.parent == null ? null : split("/", var.parent)[1] + prefix = var.prefix == null ? "" : "${var.prefix}-" + project = ( + var.project_create ? + { + project_id = try(google_project.project.0.project_id, null) + number = try(google_project.project.0.number, null) + name = try(google_project.project.0.name, null) + } + : { + project_id = "${local.prefix}${var.name}" + number = try(data.google_project.project.0.number, null) + name = try(data.google_project.project.0.name, null) + } + ) +} + +data "google_project" "project" { + count = var.project_create ? 0 : 1 + project_id = "${local.prefix}${var.name}" +} + +resource "google_project" "project" { + count = var.project_create ? 1 : 0 + org_id = local.parent_type == "organizations" ? local.parent_id : null + folder_id = local.parent_type == "folders" ? local.parent_id : null + project_id = "${local.prefix}${var.name}" + name = local.descriptive_name + billing_account = var.billing_account + auto_create_network = var.auto_create_network + labels = var.labels + skip_delete = var.skip_delete +} + +resource "google_project_service" "project_services" { + for_each = toset(var.services) + project = local.project.project_id + service = each.value + disable_on_destroy = var.service_config.disable_on_destroy + disable_dependent_services = var.service_config.disable_dependent_services +} + +resource "google_compute_project_metadata_item" "oslogin_meta" { + count = var.oslogin ? 1 : 0 + project = local.project.project_id + key = "enable-oslogin" + value = "TRUE" + # depend on services or it will fail on destroy + depends_on = [google_project_service.project_services] +} + +resource "google_resource_manager_lien" "lien" { + count = var.lien_reason != "" ? 1 : 0 + parent = "projects/${local.project.number}" + restrictions = ["resourcemanager.projects.delete"] + origin = "created-by-terraform" + reason = var.lien_reason +} + +resource "google_essential_contacts_contact" "contact" { + provider = google-beta + for_each = var.contacts + parent = "projects/${local.project.project_id}" + email = each.key + language_tag = "en" + notification_category_subscriptions = each.value +} + +resource "google_monitoring_monitored_project" "primary" { + provider = google-beta + for_each = toset(var.metric_scopes) + metrics_scope = each.value + name = local.project.project_id +} diff --git a/examples/guardrails/jenkins/project-factory/modules/project/organization-policies.tf b/examples/guardrails/jenkins/project-factory/modules/project/organization-policies.tf new file mode 100644 index 0000000..6870754 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project/organization-policies.tf @@ -0,0 +1,90 @@ +/** + * 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 + * + * 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 Project-level organization policies. + +resource "google_project_organization_policy" "boolean" { + for_each = var.policy_boolean + project = local.project.project_id + constraint = each.key + + dynamic "boolean_policy" { + for_each = each.value == null ? [] : [each.value] + iterator = policy + content { + enforced = policy.value + } + } + + dynamic "restore_policy" { + for_each = each.value == null ? [""] : [] + content { + default = true + } + } +} + +resource "google_project_organization_policy" "list" { + for_each = var.policy_list + project = local.project.project_id + constraint = each.key + + dynamic "list_policy" { + for_each = each.value.status == null ? [] : [each.value] + iterator = policy + content { + inherit_from_parent = policy.value.inherit_from_parent + suggested_value = policy.value.suggested_value + dynamic "allow" { + for_each = policy.value.status ? [""] : [] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + dynamic "deny" { + for_each = policy.value.status ? [] : [""] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + } + } + + dynamic "restore_policy" { + for_each = each.value.status == null ? [true] : [] + content { + default = true + } + } +} diff --git a/examples/guardrails/jenkins/project-factory/modules/project/outputs.tf b/examples/guardrails/jenkins/project-factory/modules/project/outputs.tf new file mode 100644 index 0000000..10d0e55 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project/outputs.tf @@ -0,0 +1,87 @@ +/** + * 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 + * + * 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. + */ + +output "custom_roles" { + description = "Ids of the created custom roles." + value = { + for name, role in google_project_iam_custom_role.roles : + name => role.id + } +} + +output "name" { + description = "Project name." + value = local.project.name + depends_on = [ + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "number" { + description = "Project number." + value = local.project.number + depends_on = [ + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "project_id" { + description = "Project id." + value = "${local.prefix}${var.name}" + depends_on = [ + google_project.project, + data.google_project.project, + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "service_accounts" { + description = "Product robot service accounts in project." + value = { + cloud_services = local.service_account_cloud_services + default = local.service_accounts_default + robots = local.service_accounts_robots + } + depends_on = [ + google_project_service.project_services, + google_kms_crypto_key_iam_member.service_identity_cmek, + google_project_service_identity.jit_si, + data.google_bigquery_default_service_account.bq_sa, + data.google_storage_project_service_account.gcs_sa + ] +} + +output "sink_writer_identities" { + description = "Writer identities created for each sink." + value = { + for name, sink in google_logging_project_sink.sink : name => sink.writer_identity + } +} diff --git a/examples/guardrails/jenkins/project-factory/modules/project/service-accounts.tf b/examples/guardrails/jenkins/project-factory/modules/project/service-accounts.tf new file mode 100644 index 0000000..3423524 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project/service-accounts.tf @@ -0,0 +1,113 @@ +/** + * 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 + * + * 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 Service identities and supporting resources. + +locals { + _service_accounts_cmek_service_dependencies = { + "composer" : [ + "composer", + "artifactregistry", "container-engine", "compute", "pubsub", "storage" + ] + "dataflow" : ["dataflow", "compute"] + } + _service_accounts_robot_services = { + artifactregistry = "service-%s@gcp-sa-artifactregistry" + bq = "bq-%s@bigquery-encryption" + cloudasset = "service-%s@gcp-sa-cloudasset" + cloudbuild = "service-%s@gcp-sa-cloudbuild" + cloudfunctions = "service-%s@gcf-admin-robot" + cloudrun = "service-%s@serverless-robot-prod" + composer = "service-%s@cloudcomposer-accounts" + compute = "service-%s@compute-system" + container-engine = "service-%s@container-engine-robot" + containerregistry = "service-%s@containerregistry" + dataflow = "service-%s@dataflow-service-producer-prod" + dataproc = "service-%s@dataproc-accounts" + gae-flex = "service-%s@gae-api-prod" + # TODO: deprecate gcf + gcf = "service-%s@gcf-admin-robot" + pubsub = "service-%s@gcp-sa-pubsub" + secretmanager = "service-%s@gcp-sa-secretmanager" + storage = "service-%s@gs-project-accounts" + } + service_accounts_default = { + compute = "${local.project.number}-compute@developer.gserviceaccount.com" + gae = "${local.project.project_id}@appspot.gserviceaccount.com" + } + service_account_cloud_services = ( + "${local.project.number}@cloudservices.gserviceaccount.com" + ) + service_accounts_robots = { + for k, v in local._service_accounts_robot_services : + k => "${format(v, local.project.number)}.iam.gserviceaccount.com" + } + service_accounts_jit_services = [ + "secretmanager.googleapis.com", + "pubsub.googleapis.com", + "cloudasset.googleapis.com" + ] + service_accounts_cmek_service_keys = distinct(flatten([ + for s in keys(var.service_encryption_key_ids) : [ + for ss in try(local._service_accounts_cmek_service_dependencies[s], [s]) : [ + for key in var.service_encryption_key_ids[s] : { + service = ss + key = key + } if key != null + ] + ] + ])) +} + +data "google_storage_project_service_account" "gcs_sa" { + count = contains(var.services, "storage.googleapis.com") ? 1 : 0 + project = local.project.project_id + depends_on = [google_project_service.project_services] +} + +data "google_bigquery_default_service_account" "bq_sa" { + count = contains(var.services, "bigquery.googleapis.com") ? 1 : 0 + project = local.project.project_id + depends_on = [google_project_service.project_services] +} + +# Secret Manager SA created just in time, we need to trigger the creation. +resource "google_project_service_identity" "jit_si" { + for_each = setintersection(var.services, local.service_accounts_jit_services) + provider = google-beta + project = local.project.project_id + service = each.value + depends_on = [google_project_service.project_services] +} + +resource "google_kms_crypto_key_iam_member" "service_identity_cmek" { + for_each = { + for service_key in local.service_accounts_cmek_service_keys : + "${service_key.service}.${service_key.key}" => service_key + if service_key != service_key.key + } + crypto_key_id = each.value.key + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + member = "serviceAccount:${local.service_accounts_robots[each.value.service]}" + depends_on = [ + google_project.project, + google_project_service.project_services, + google_project_service_identity.jit_si, + data.google_bigquery_default_service_account.bq_sa, + data.google_project.project, + data.google_storage_project_service_account.gcs_sa, + ] +} diff --git a/examples/guardrails/jenkins/project-factory/modules/project/shared-vpc.tf b/examples/guardrails/jenkins/project-factory/modules/project/shared-vpc.tf new file mode 100644 index 0000000..9c7bd71 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project/shared-vpc.tf @@ -0,0 +1,75 @@ +/** + * 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 + * + * 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 Shared VPC project-level configuration. + +locals { + # compute the host project IAM bindings for this project's service identities + _svpc_service_identity_iam = coalesce( + local.svpc_service_config.service_identity_iam, {} + ) + _svpc_service_iam = flatten([ + for role, services in local._svpc_service_identity_iam : [ + for service in services : { role = role, service = service } + ] + ]) + svpc_host_config = { + enabled = coalesce( + try(var.shared_vpc_host_config.enabled, null), false + ) + service_projects = coalesce( + try(var.shared_vpc_host_config.service_projects, null), [] + ) + } + svpc_service_config = coalesce(var.shared_vpc_service_config, { + host_project = null, service_identity_iam = {} + }) + svpc_service_iam = { + for b in local._svpc_service_iam : "${b.role}:${b.service}" => b + } +} + +resource "google_compute_shared_vpc_host_project" "shared_vpc_host" { + provider = google-beta + count = local.svpc_host_config.enabled ? 1 : 0 + project = local.project.project_id +} + +resource "google_compute_shared_vpc_service_project" "service_projects" { + provider = google-beta + for_each = toset(local.svpc_host_config.service_projects) + host_project = local.project.project_id + service_project = each.value + depends_on = [google_compute_shared_vpc_host_project.shared_vpc_host] +} + +resource "google_compute_shared_vpc_service_project" "shared_vpc_service" { + provider = google-beta + count = local.svpc_service_config.host_project != null ? 1 : 0 + host_project = var.shared_vpc_service_config.host_project + service_project = local.project.project_id +} + +resource "google_project_iam_member" "shared_vpc_host_robots" { + for_each = local.svpc_service_iam + project = var.shared_vpc_service_config.host_project + role = each.value.role + member = ( + each.value.service == "cloudservices" + ? "serviceAccount:${local.service_account_cloud_services}" + : "serviceAccount:${local.service_accounts_robots[each.value.service]}" + ) +} diff --git a/examples/guardrails/jenkins/project-factory/modules/project/tags.tf b/examples/guardrails/jenkins/project-factory/modules/project/tags.tf new file mode 100644 index 0000000..683143b --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project/tags.tf @@ -0,0 +1,21 @@ +/** + * 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 + * + * 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. + */ + +resource "google_tags_tag_binding" "binding" { + for_each = coalesce(var.tag_bindings, {}) + parent = "//cloudresourcemanager.googleapis.com/projects/${local.project.number}" + tag_value = each.value +} diff --git a/examples/guardrails/jenkins/project-factory/modules/project/variables.tf b/examples/guardrails/jenkins/project-factory/modules/project/variables.tf new file mode 100644 index 0000000..578f9d2 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project/variables.tf @@ -0,0 +1,259 @@ +/** + * 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 + * + * 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. + */ + +variable "auto_create_network" { + description = "Whether to create the default network for the project." + type = bool + default = false +} + +variable "billing_account" { + description = "Billing account id." + type = string + default = null +} + +variable "contacts" { + description = "List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES." + type = map(list(string)) + default = {} + nullable = false +} + +variable "custom_roles" { + description = "Map of role name => list of permissions to create in this project." + type = map(list(string)) + default = {} + nullable = false +} + +variable "descriptive_name" { + description = "Name of the project name. Used for project name instead of `name` variable." + type = string + default = null +} + +variable "group_iam" { + description = "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." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam" { + description = "IAM bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_additive" { + description = "IAM additive bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +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 "labels" { + description = "Resource labels." + type = map(string) + default = {} + nullable = false +} + +variable "lien_reason" { + description = "If non-empty, creates a project lien with this description." + type = string + default = "" +} + +variable "logging_exclusions" { + description = "Logging exclusions for this project in the form {NAME -> FILTER}." + type = map(string) + default = {} + nullable = false +} + +variable "logging_sinks" { + description = "Logging sinks to create for this project." + type = map(object({ + destination = string + type = string + filter = string + iam = bool + unique_writer = bool + # TODO exclusions also support description and disabled + exclusions = map(string) + })) + validation { + condition = alltrue([ + for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } + default = {} + nullable = false +} + +variable "metric_scopes" { + description = "List of projects that will act as metric scopes for this project." + type = list(string) + default = [] + nullable = false +} + +variable "name" { + description = "Project name and id suffix." + type = string +} + +variable "oslogin" { + description = "Enable OS Login." + type = bool + default = false +} + +variable "oslogin_admins" { + description = "List of IAM-style identities that will be granted roles necessary for OS Login administrators." + type = list(string) + default = [] + nullable = false + +} + +variable "oslogin_users" { + description = "List of IAM-style identities that will be granted roles necessary for OS Login users." + type = list(string) + default = [] + nullable = false +} + +variable "parent" { + description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "policy_boolean" { + description = "Map of boolean org policies and enforcement value, set value to null for policy restore." + type = map(bool) + default = {} + nullable = false +} + +variable "policy_list" { + description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." + type = map(object({ + inherit_from_parent = bool + suggested_value = string + status = bool + values = list(string) + })) + default = {} + nullable = false +} + +variable "prefix" { + description = "Prefix used to generate project id and name." + type = string + default = null +} + +variable "project_create" { + description = "Create project. When set to false, uses a data source to reference existing project." + type = bool + default = true +} + +variable "service_config" { + description = "Configure service API activation." + type = object({ + disable_on_destroy = bool + disable_dependent_services = bool + }) + default = { + disable_on_destroy = true + disable_dependent_services = true + } +} + +variable "service_encryption_key_ids" { + description = "Cloud KMS encryption key in {SERVICE => [KEY_URL]} format." + type = map(list(string)) + default = {} +} + +# accessPolicies/ACCESS_POLICY_NAME/servicePerimeters/PERIMETER_NAME +variable "service_perimeter_bridges" { + description = "Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format." + type = list(string) + default = null +} + +# accessPolicies/ACCESS_POLICY_NAME/servicePerimeters/PERIMETER_NAME +variable "service_perimeter_standard" { + description = "Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format." + type = string + default = null +} + +variable "services" { + description = "Service APIs to enable." + type = list(string) + default = [] +} + +variable "shared_vpc_host_config" { + description = "Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project)." + type = object({ + enabled = bool + service_projects = list(string) + }) + default = null +} + +variable "shared_vpc_service_config" { + description = "Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config)." + # the list of valid service identities is in service-accounts.tf + type = object({ + host_project = string + service_identity_iam = map(list(string)) + }) + default = null +} + +variable "skip_delete" { + description = "Allows the underlying resources to be destroyed without destroying the project itself." + type = bool + default = false +} + +variable "tag_bindings" { + description = "Tag bindings for this project, in key => tag value id format." + type = map(string) + default = null +} diff --git a/examples/guardrails/jenkins/project-factory/modules/project/versions.tf b/examples/guardrails/jenkins/project-factory/modules/project/versions.tf new file mode 100644 index 0000000..e72a780 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.1.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/jenkins/project-factory/modules/project/vpc-sc.tf b/examples/guardrails/jenkins/project-factory/modules/project/vpc-sc.tf new file mode 100644 index 0000000..edaa203 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project/vpc-sc.tf @@ -0,0 +1,45 @@ +/** + * 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 + * + * 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 VPC-SC project-level perimeter configuration. + +moved { + from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-standard + to = google_access_context_manager_service_perimeter_resource.standard +} + +resource "google_access_context_manager_service_perimeter_resource" "standard" { + count = var.service_perimeter_standard != null ? 1 : 0 + # this needs an additional lifecycle block in the vpc module on the + # google_access_context_manager_service_perimeter resource + perimeter_name = var.service_perimeter_standard + resource = "projects/${local.project.number}" +} + +moved { + from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-bridges + to = google_access_context_manager_service_perimeter_resource.bridge +} + +resource "google_access_context_manager_service_perimeter_resource" "bridge" { + for_each = toset( + var.service_perimeter_bridges != null ? var.service_perimeter_bridges : [] + ) + # this needs an additional lifecycle block in the vpc module on the + # google_access_context_manager_service_perimeter resource + perimeter_name = each.value + resource = "projects/${local.project.number}" +} diff --git a/examples/guardrails/jenkins/project-factory/modules/project_plus/README.md b/examples/guardrails/jenkins/project-factory/modules/project_plus/README.md new file mode 100644 index 0000000..2976a30 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project_plus/README.md @@ -0,0 +1 @@ +This is an addon for the project module with connects one service account to one project. \ No newline at end of file diff --git a/examples/guardrails/jenkins/project-factory/modules/project_plus/main.tf b/examples/guardrails/jenkins/project-factory/modules/project_plus/main.tf new file mode 100644 index 0000000..2332f16 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project_plus/main.tf @@ -0,0 +1,46 @@ +/** + * 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 + * + * 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. + */ + +resource "random_id" "rand" { + byte_length = 4 +} + +module "project" { + source = "./../project" + name = "${var.team}-prj-${random_id.rand.hex}" + parent = var.folder + billing_account = var.billing_account +} + +resource "google_service_account" "sa" { + account_id = "${var.team}-sa-${random_id.rand.hex}" + display_name = "Service account ${var.team}" + project = module.project.project_id +} + +resource "google_service_account_iam_member" "sa-iam" { + service_account_id = google_service_account.sa.name + role = "roles/iam.workloadIdentityUser" + member = "principalSet://iam.googleapis.com/${var.wif-pool}/attribute.branch_name/${var.repo_sub}" +} + +resource "google_project_iam_member" "sa-project" { + for_each = toset(var.roles) + role = each.value + member = "serviceAccount:${google_service_account.sa.email}" + project = module.project.project_id + depends_on = [google_service_account.sa] +} diff --git a/examples/guardrails/jenkins/project-factory/modules/project_plus/outputs.tf b/examples/guardrails/jenkins/project-factory/modules/project_plus/outputs.tf new file mode 100644 index 0000000..c589d87 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project_plus/outputs.tf @@ -0,0 +1,36 @@ +/** + * 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 + * + * 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. + */ + +output "project_id" { + description = "Project ID" + value = module.project.project_id +} + +output "service_account_email" { + description = "Service Account Email" + value = google_service_account.sa.email +} + +output "repo_sub" { + description = "Repository" + value = var.repo_sub +} + +output "repo_provider" { + description = "Repository Provider" + value = var.repo_provider +} + diff --git a/examples/guardrails/jenkins/project-factory/modules/project_plus/variables.tf b/examples/guardrails/jenkins/project-factory/modules/project_plus/variables.tf new file mode 100644 index 0000000..67524ae --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project_plus/variables.tf @@ -0,0 +1,52 @@ +/** + * 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 + * + * 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. + */ + + +variable "team" { + description = "Team name." + type = string +} + +variable "repo_sub" { + description = "Repository path" + type = string +} + +variable "repo_provider" { + description = "Repository provider" + type = string +} + +variable "billing_account" { + description = "Billing account name." + type = string +} + +variable "folder" { + description = "Folder name." + type = string +} + +variable "roles" { + description = "Roles to attach." + type = list(string) + default = [] +} + +variable "wif-pool" { + description = "WIF pool name." + type = string +} diff --git a/examples/guardrails/jenkins/project-factory/modules/project_plus/versions.tf b/examples/guardrails/jenkins/project-factory/modules/project_plus/versions.tf new file mode 100644 index 0000000..2904126 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/modules/project_plus/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.0.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/jenkins/project-factory/outputs.tf b/examples/guardrails/jenkins/project-factory/outputs.tf new file mode 100644 index 0000000..43d9c4f --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/outputs.tf @@ -0,0 +1,40 @@ +/** + * 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 + * + * 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. + */ + +output "wif_pool_id_gitlab" { + description = "Gitlab Workload Identity Pool" + value = google_iam_workload_identity_pool.wif-pool-gitlab.name +} + +output "wif_provider_id_gitlab" { + description = "Gitlab Workload Identity Pool" + value = google_iam_workload_identity_pool_provider.wif-provider-gitlab.name +} + +output "wif_pool_id_github" { + description = "Github Workload Identity Pool" + value = google_iam_workload_identity_pool.wif-pool-github.name +} + +output "wif_provider_id_github" { + description = "Github Workload Identity Pool" + value = google_iam_workload_identity_pool_provider.wif-provider-github.name +} + +output "projects" { + description = "Created projects and service accounts." + value = module.project +} \ No newline at end of file diff --git a/examples/guardrails/jenkins/project-factory/provider.tf b/examples/guardrails/jenkins/project-factory/provider.tf new file mode 100644 index 0000000..34beb2f --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/provider.tf @@ -0,0 +1,28 @@ +/** + * 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 + * + * 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. + */ + +terraform { + backend "gcs" { + } +} + +provider "google" { + +} + +provider "google-beta" { + +} diff --git a/examples/guardrails/jenkins/project-factory/variables.tf b/examples/guardrails/jenkins/project-factory/variables.tf new file mode 100644 index 0000000..c2c9ea4 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/variables.tf @@ -0,0 +1,27 @@ +/** + * 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 + * + * 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. + */ + +variable "folder" { + +} +variable "billing_account" { +} +variable "issuer_uri" { +} + +variable "allowed_audiences" { + type = list +} diff --git a/examples/guardrails/jenkins/project-factory/wif.tf b/examples/guardrails/jenkins/project-factory/wif.tf new file mode 100644 index 0000000..d3568e4 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/wif.tf @@ -0,0 +1,51 @@ +/** + * 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 + * + * 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. + */ + +resource "random_id" "rand" { + byte_length = 4 +} + +module "wif-project" { + source = "./modules/project" + name = "wif-prj-${random_id.rand.hex}" + parent = var.folder + billing_account = var.billing_account +} + +resource "google_iam_workload_identity_pool" "wif-pool-jenkins" { + provider = google-beta + workload_identity_pool_id = "jenkins-pool1-${random_id.rand.hex}" + project = module.wif-project.project_id +} + +resource "google_iam_workload_identity_pool_provider" "wif-provider-jenkins" { + provider = google-beta + workload_identity_pool_id = google_iam_workload_identity_pool.wif-pool-jenkins.workload_identity_pool_id + workload_identity_pool_provider_id = "jenkins-provider-${random_id.rand.hex}" + project = module.wif-project.project_id + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.sub" = "assertion.sub" + "attribute.branch_name" = "assertion.branchName" + } + oidc { + issuer_uri = var.issuer_uri + allowed_audiences = var.allowed_audiences + } + depends_on = [ + google_iam_workload_identity_pool.wif-pool-jenkins + ] +} diff --git a/examples/guardrails/jenkins/skunkworks/.github/workflows/tf-actions-dev.yml b/examples/guardrails/jenkins/skunkworks/.github/workflows/tf-actions-dev.yml new file mode 100644 index 0000000..2fbf4c1 --- /dev/null +++ b/examples/guardrails/jenkins/skunkworks/.github/workflows/tf-actions-dev.yml @@ -0,0 +1,91 @@ +# 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 +# +# 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. + +name: 'DEV Deployment' + +on: + push: + branches: + - dev + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.DEV_SERVICE_ACCOUNT }} + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve + + diff --git a/examples/guardrails/jenkins/skunkworks/.github/workflows/tf-actions-prod.yml b/examples/guardrails/jenkins/skunkworks/.github/workflows/tf-actions-prod.yml new file mode 100644 index 0000000..4159feb --- /dev/null +++ b/examples/guardrails/jenkins/skunkworks/.github/workflows/tf-actions-prod.yml @@ -0,0 +1,91 @@ +# 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 +# +# 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. + +name: 'PROD Deployment' + +on: + push: + branches: + - main + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.PROD_SERVICE_ACCOUNT }} + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve + + diff --git a/examples/guardrails/jenkins/skunkworks/.github/workflows/tf-actions-stage.yml b/examples/guardrails/jenkins/skunkworks/.github/workflows/tf-actions-stage.yml new file mode 100644 index 0000000..0f20fa0 --- /dev/null +++ b/examples/guardrails/jenkins/skunkworks/.github/workflows/tf-actions-stage.yml @@ -0,0 +1,91 @@ +# 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 +# +# 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. + +name: 'STAGE Deployment' + +on: + push: + branches: + - stage + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.STAGE_SERVICE_ACCOUNT }} + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve + + diff --git a/examples/guardrails/jenkins/skunkworks/Jenkinsfile b/examples/guardrails/jenkins/skunkworks/Jenkinsfile new file mode 100644 index 0000000..099eed3 --- /dev/null +++ b/examples/guardrails/jenkins/skunkworks/Jenkinsfile @@ -0,0 +1,65 @@ +pipeline { + + agent any + environment { + BUCKET_PATH = credentials('backend-path') + SKUNKWORK_PROJECT = credentials('staging-project') + SW_PROJECT_NUMBER = credentials('sw-project-number') + SW_SERVICE_ACCOUNT = credentials('sw-sa') + } + options { + skipDefaultCheckout(true) + } + stages { + stage('clean workspace') { + steps { + cleanWs() + } + } + stage('WIF') { + steps { + withCredentials([file(variable: 'ID_TOKEN_STAGE', credentialsId: 'staging')]) { + writeFile file: "$WORKSPACE_TMP/sts_creds.json", text: """ + { + "type": "external_account", + "audience": "//iam.googleapis.com/projects/${SW_PROJECT_NUMBER}/locations/global/workloadIdentityPools/jenkins-pool1-ad8aa16a/providers/jenkins-provider-ad8aa16a", + "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", + "token_url": "https://sts.googleapis.com/v1/token", + "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SW_SERVICE_ACCOUNT}@${SKUNKWORK_PROJECT}.iam.gserviceaccount.com:generateAccessToken", + "credential_source": { + "file": "$ID_TOKEN_STAGE", + "format": { + "type": "text" + } + } + } + """ + sh ''' + gcloud auth login --brief --cred-file=$WORKSPACE_TMP/sts_creds.json + ''' +} + } + } + + stage('checkout') { + steps { + checkout scm + } + } + stage('Terraform') { + steps { + + sh ''' + cd guardrails/skunkworks + terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=prod-skunkworks" + terraform plan -var="project=${SKUNKWORK_PROJECT}" + terraform apply -var="project=${SKUNKWORK_PROJECT}" -auto-approve + + ''' + + } + } + + + } +} diff --git a/examples/guardrails/jenkins/skunkworks/README.md b/examples/guardrails/jenkins/skunkworks/README.md new file mode 100644 index 0000000..0b89bb2 --- /dev/null +++ b/examples/guardrails/jenkins/skunkworks/README.md @@ -0,0 +1,29 @@ +# Skunkworks - IaC Kickstarter Template + +This is a template for an IaC kickstarter repository. + +![Skunkworks](https://user-images.githubusercontent.com/94000358/169810982-36f01de2-e5e5-4ecd-b98e-3cf5a6aa9f81.png) + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +This template creates a bucket in the specified target environment. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: + +Secret configuration + +or by modifing the [Workflow Action Files](.github/workflows/) and setting the environment variables: +``` +env: + STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' + # The workload identity provider that should be used for this repository. + SERVICE_ACCOUNT: 'XXXX@XXXX' + # The service account that should be used for this repository. +``` diff --git a/examples/guardrails/jenkins/skunkworks/main.tf b/examples/guardrails/jenkins/skunkworks/main.tf new file mode 100644 index 0000000..03926bd --- /dev/null +++ b/examples/guardrails/jenkins/skunkworks/main.tf @@ -0,0 +1,23 @@ +/** + * 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 + * + * 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. + */ + +resource "google_storage_bucket" "bucket" { + project = var.project + name = lower("${var.project}-test-bucket") + location = "EU" + force_destroy = true +} + diff --git a/examples/guardrails/jenkins/skunkworks/provider.tf b/examples/guardrails/jenkins/skunkworks/provider.tf new file mode 100644 index 0000000..0802a09 --- /dev/null +++ b/examples/guardrails/jenkins/skunkworks/provider.tf @@ -0,0 +1,28 @@ +/** + * 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 + * + * 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. + */ + +terraform { + backend "gcs" { + } +} + +provider "google" { + +} + +provider "google-beta" { + +} \ No newline at end of file diff --git a/examples/guardrails/jenkins/skunkworks/variables.tf b/examples/guardrails/jenkins/skunkworks/variables.tf new file mode 100644 index 0000000..2709299 --- /dev/null +++ b/examples/guardrails/jenkins/skunkworks/variables.tf @@ -0,0 +1,20 @@ +/** + * 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 + * + * 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. + */ + +variable "project" { + type = string + +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/.github/workflows/terraform-deployment.yml b/examples/guardrails/terraform-cloud/folder-factory/.github/workflows/terraform-deployment.yml new file mode 100644 index 0000000..70f88cb --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/.github/workflows/terraform-deployment.yml @@ -0,0 +1,91 @@ +# 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 +# +# 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. + +name: 'Cloud Deployment' + +on: + push: + branches: + - main + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve + + diff --git a/examples/guardrails/terraform-cloud/folder-factory/.gitignore b/examples/guardrails/terraform-cloud/folder-factory/.gitignore new file mode 100644 index 0000000..a5d8c0e --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.terraform +.terraform.lock.hcl diff --git a/examples/guardrails/terraform-cloud/folder-factory/README.md b/examples/guardrails/terraform-cloud/folder-factory/README.md new file mode 100644 index 0000000..bd6fd96 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/README.md @@ -0,0 +1,66 @@ +# Folder Factory + +This is a template for a DevOps folder factory. + +It can be used with [https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory](https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory) and is intended to house the folder configurations: + +![Screenshot 2022-05-10 12 00 19 PM](https://user-images.githubusercontent.com/94000358/169809437-aaa8538e-3ffc-48b3-9028-84e4995de150.png) + +Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +## Setting Up Terraform Wokspace on Terraform Cloud + + +- Ensure to have a Workspace created on terraform Cloud which would have Gitlab Repository as the VCS Source. + +- Update the [data](data/folders/) with pre created organisation ID and Service account. + +- Update the variables for Terraform Workspace as below + +``` +env: + impersonate_service_account_email: 'xxx@project.iam.gserviceaccount.com' + # The Service Account used to create Folder. + project_id: 'xxxx' + # Project ID which will host provider aand pool + TFC_WORKLOAD_IDENTITY_AUDIENCE: '//iam.googleapis.com/projects/id/locations/global/workloadIdentityPools//providers/' + # WorkLoad Identity Audience will be used by tfc-oidc module for token generation and impersonation +``` + + +> **_NOTE:_** You need to have TFC Workspace ID & TFC Organisation ID created, before it can be passed in [terraform-cloud-wif](terraform-cloud-wif) module to generate the Provider, Pool, Service account & IAM Role. This IAM Role would be attached to the Service Account allowing authorization. + +## Setting up folders + +The folder factory will: +- create a folders with defined organisational policies + +It uses YAML configuration files for every folder with the following sample structure: +``` +parent: folders/XXXXXXXXX +org_policies: + policy_boolean: + constraints/compute.disableGuestAttributesAccess: true + constraints/iam.disableServiceAccountCreation: false + constraints/iam.disableServiceAccountKeyCreation: false + constraints/iam.disableServiceAccountKeyUpload: false + constraints/gcp.disableCloudLogging: false + policy_list: + constraints/compute.vmExternalIpAccess: + inherit_from_parent: null + status: true + suggested_value: null + values: +iam: + roles/resourcemanager.projectCreator: + - serviceAccount:XXXXX@XXXXXX +``` + +Every folder is defined with its own yaml file located in the following [Folder](data/folder). diff --git a/examples/guardrails/terraform-cloud/folder-factory/data/folders/app1.yaml b/examples/guardrails/terraform-cloud/folder-factory/data/folders/app1.yaml new file mode 100644 index 0000000..2fb7c85 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/data/folders/app1.yaml @@ -0,0 +1,31 @@ +# 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 +# +# 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. + +parent: organizations/555271503501 +org_policies: + policy_boolean: + constraints/compute.disableGuestAttributesAccess: true + constraints/iam.disableServiceAccountCreation: false + constraints/iam.disableServiceAccountKeyCreation: false + constraints/iam.disableServiceAccountKeyUpload: false + constraints/gcp.disableCloudLogging: false + policy_list: + constraints/compute.vmExternalIpAccess: + inherit_from_parent: null + status: true + suggested_value: null + values: +iam: + roles/resourcemanager.projectCreator: + - serviceAccount:sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/folder-factory/data/folders/app2.yaml b/examples/guardrails/terraform-cloud/folder-factory/data/folders/app2.yaml new file mode 100644 index 0000000..2fb7c85 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/data/folders/app2.yaml @@ -0,0 +1,31 @@ +# 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 +# +# 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. + +parent: organizations/555271503501 +org_policies: + policy_boolean: + constraints/compute.disableGuestAttributesAccess: true + constraints/iam.disableServiceAccountCreation: false + constraints/iam.disableServiceAccountKeyCreation: false + constraints/iam.disableServiceAccountKeyUpload: false + constraints/gcp.disableCloudLogging: false + policy_list: + constraints/compute.vmExternalIpAccess: + inherit_from_parent: null + status: true + suggested_value: null + values: +iam: + roles/resourcemanager.projectCreator: + - serviceAccount:sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/folder-factory/main.tf b/examples/guardrails/terraform-cloud/folder-factory/main.tf new file mode 100644 index 0000000..f21d44e --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/main.tf @@ -0,0 +1,32 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + folders = { + for f in fileset("./data/folders", "**/*.yaml") : + trimsuffix(f, ".yaml") => yamldecode(file("./data/folders/${f}")) + } +} + +module "folder" { + source = "./modules/folder" + for_each = local.folders + name = each.key + parent = each.value.parent + policy_boolean = try(each.value.org_policies.policy_boolean, {}) + policy_list = try(each.value.org_policies.policy_list, {}) + iam = try(each.value.iam, {}) +} \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/folder-factory/modules/folder/README.md b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/README.md new file mode 100644 index 0000000..4f6898b --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/README.md @@ -0,0 +1,299 @@ +# Google Cloud Folder Module + +This module allows the creation and management of folders, including support for IAM bindings, organization policies, and hierarchical firewall rules. + +## Examples + +### IAM bindings + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + group_iam = { + "cloud-owners@example.org" = [ + "roles/owner", + "roles/resourcemanager.projectCreator" + ] + } + iam = { + "roles/owner" = ["user:one@example.com"] + } +} +# tftest modules=1 resources=3 +``` + +### Organization policies + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + policy_boolean = { + "constraints/compute.disableGuestAttributesAccess" = true + "constraints/compute.skipDefaultNetworkCreation" = true + } + policy_list = { + "constraints/compute.trustedImageProjects" = { + inherit_from_parent = null + suggested_value = null + status = true + values = ["projects/my-project"] + } + } +} +# tftest modules=1 resources=4 +``` + +### Firewall policy factory + +In the same way as for the [organization](../organization) module, the in-built factory allows you to define a single policy, using one file for rules, and an optional file for CIDR range substitution variables. Remember that non-absolute paths are relative to the root module (the folder where you run `terraform`). + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + firewall_policy_factory = { + cidr_file = "data/cidrs.yaml" + policy_name = null + rules_file = "data/rules.yaml" + } + firewall_policy_association = { + factory-policy = module.folder.firewall_policy_id["factory"] + } +} +# tftest skip +``` + +```yaml +# cidrs.yaml + +rfc1918: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 +``` + +```yaml +# rules.yaml + +allow-admins: + description: Access from the admin subnet to all subnets + direction: INGRESS + action: allow + priority: 1000 + ranges: + - $rfc1918 + ports: + all: [] + target_resources: null + enable_logging: false + +allow-ssh-from-iap: + description: Enable SSH from IAP + direction: INGRESS + action: allow + priority: 1002 + ranges: + - 35.235.240.0/20 + ports: + tcp: ["22"] + target_resources: null + enable_logging: false +``` + +### Logging Sinks + +```hcl +module "gcs" { + source = "./modules/gcs" + project_id = "my-project" + name = "gcs_sink" + force_destroy = true +} + +module "dataset" { + source = "./modules/bigquery-dataset" + project_id = "my-project" + id = "bq_sink" +} + +module "pubsub" { + source = "./modules/pubsub" + project_id = "my-project" + name = "pubsub_sink" +} + +module "bucket" { + source = "./modules/logging-bucket" + parent_type = "project" + parent = "my-project" + id = "bucket" +} + +module "folder-sink" { + source = "./modules/folder" + parent = "folders/657104291943" + name = "my-folder" + logging_sinks = { + warnings = { + type = "storage" + destination = module.gcs.name + filter = "severity=WARNING" + include_children = true + exclusions = {} + } + info = { + type = "bigquery" + destination = module.dataset.id + filter = "severity=INFO" + include_children = true + exclusions = {} + } + notice = { + type = "pubsub" + destination = module.pubsub.id + filter = "severity=NOTICE" + include_children = true + exclusions = {} + } + debug = { + type = "logging" + destination = module.bucket.id + filter = "severity=DEBUG" + include_children = true + exclusions = { + no-compute = "logName:compute" + } + } + } + logging_exclusions = { + no-gce-instances = "resource.type=gce_instance" + } +} +# tftest modules=5 resources=14 +``` + +### Hierarchical firewall policies + +```hcl +module "folder1" { + source = "./modules/folder" + parent = var.organization_id + name = "policy-container" + + firewall_policies = { + iap-policy = { + allow-iap-ssh = { + description = "Always allow ssh from IAP" + direction = "INGRESS" + action = "allow" + priority = 100 + ranges = ["35.235.240.0/20"] + ports = { tcp = ["22"] } + target_service_accounts = null + target_resources = null + logging = false + } + } + } + firewall_policy_association = { + iap-policy = "iap-policy" + } +} + +module "folder2" { + source = "./modules/folder" + parent = var.organization_id + name = "hf2" + firewall_policy_association = { + iap-policy = module.folder1.firewall_policy_id["iap-policy"] + } +} +# tftest modules=2 resources=6 +``` + +## 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. + +```hcl +module "org" { + source = "./modules/organization" + organization_id = var.organization_id + tags = { + environment = { + description = "Environment specification." + iam = null + values = { + dev = null + prod = null + } + } + } +} + +module "folder" { + source = "./modules/folder" + name = "Test" + parent = module.org.organization_id + tag_bindings = { + env-prod = module.org.tag_values["environment/prod"].id + foo = "tagValues/12345678" + } +} +# tftest modules=2 resources=6 +``` + + + + +## Files + +| name | description | resources | +|---|---|---| +| [firewall-policies.tf](./firewall-policies.tf) | None | google_compute_firewall_policy · google_compute_firewall_policy_association · google_compute_firewall_policy_rule | +| [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_folder_iam_binding | +| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_folder_exclusion · google_logging_folder_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | +| [main.tf](./main.tf) | Module-level locals and resources. | google_essential_contacts_contact · google_folder | +| [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_folder_organization_policy | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding | +| [variables.tf](./variables.tf) | Module variables. | | +| [versions.tf](./versions.tf) | Version pins. | | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | +| [firewall_policies](variables.tf#L24) | Hierarchical firewall policies created in this folder. | map(map(object({…}))) | | {} | +| [firewall_policy_association](variables.tf#L41) | The hierarchical firewall policy to associate to this folder. Must be either a key in the `firewall_policies` map or the id of a policy defined somewhere else. | map(string) | | {} | +| [firewall_policy_factory](variables.tf#L48) | Configuration for the firewall policy factory. | object({…}) | | null | +| [folder_create](variables.tf#L58) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | +| [group_iam](variables.tf#L64) | 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. | map(list(string)) | | {} | +| [iam](variables.tf#L71) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [id](variables.tf#L78) | Folder ID in case you use folder_create=false. | string | | null | +| [logging_exclusions](variables.tf#L84) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L91) | Logging sinks to create for this folder. | map(object({…})) | | {} | +| [name](variables.tf#L112) | Folder name. | string | | null | +| [parent](variables.tf#L118) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [policy_boolean](variables.tf#L128) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | +| [policy_list](variables.tf#L135) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L147) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [firewall_policies](outputs.tf#L16) | Map of firewall policy resources created in this folder. | | +| [firewall_policy_id](outputs.tf#L21) | Map of firewall policy ids created in this folder. | | +| [folder](outputs.tf#L26) | Folder resource. | | +| [id](outputs.tf#L31) | Folder id. | | +| [name](outputs.tf#L41) | Folder name. | | +| [sink_writer_identities](outputs.tf#L46) | Writer identities created for each sink. | | + + diff --git a/examples/guardrails/terraform-cloud/folder-factory/modules/folder/firewall-policies.tf b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/firewall-policies.tf new file mode 100644 index 0000000..96224c5 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/firewall-policies.tf @@ -0,0 +1,93 @@ +/** + * 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 + * + * 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 { + _factory_cidrs = try( + yamldecode(file(var.firewall_policy_factory.cidr_file)), {} + ) + _factory_name = ( + try(var.firewall_policy_factory.policy_name, null) == null + ? "factory" + : var.firewall_policy_factory.policy_name + ) + _factory_rules = try( + yamldecode(file(var.firewall_policy_factory.rules_file)), {} + ) + _factory_rules_parsed = { + for name, rule in local._factory_rules : name => merge(rule, { + ranges = flatten([ + for r in(rule.ranges == null ? [] : rule.ranges) : + lookup(local._factory_cidrs, trimprefix(r, "$"), r) + ]) + }) + } + _merged_rules = flatten([ + for policy, rules in local.firewall_policies : [ + for name, rule in rules : merge(rule, { + policy = policy + name = name + }) + ] + ]) + firewall_policies = merge(var.firewall_policies, ( + length(local._factory_rules) == 0 + ? {} + : { (local._factory_name) = local._factory_rules_parsed } + )) + firewall_rules = { + for r in local._merged_rules : "${r.policy}-${r.name}" => r + } +} + +resource "google_compute_firewall_policy" "policy" { + for_each = local.firewall_policies + short_name = each.key + parent = local.folder.id +} + +resource "google_compute_firewall_policy_rule" "rule" { + for_each = local.firewall_rules + firewall_policy = google_compute_firewall_policy.policy[each.value.policy].id + action = each.value.action + direction = each.value.direction + priority = try(each.value.priority, null) + target_resources = try(each.value.target_resources, null) + target_service_accounts = try(each.value.target_service_accounts, null) + enable_logging = try(each.value.logging, null) + # preview = each.value.preview + description = each.value.description + match { + src_ip_ranges = each.value.direction == "INGRESS" ? each.value.ranges : null + dest_ip_ranges = each.value.direction == "EGRESS" ? each.value.ranges : null + dynamic "layer4_configs" { + for_each = each.value.ports + iterator = port + content { + ip_protocol = port.key + ports = port.value + } + } + } +} + + +resource "google_compute_firewall_policy_association" "association" { + for_each = var.firewall_policy_association + name = replace(local.folder.id, "/", "-") + attachment_target = local.folder.id + firewall_policy = try(google_compute_firewall_policy.policy[each.value].id, each.value) +} + diff --git a/examples/guardrails/terraform-cloud/folder-factory/modules/folder/iam.tf b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/iam.tf new file mode 100644 index 0000000..52886ba --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/iam.tf @@ -0,0 +1,40 @@ +/** + * 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 + * + * 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 IAM bindings, roles and audit logging resources. + +locals { + group_iam_roles = distinct(flatten(values(var.group_iam))) + group_iam = { + for r in local.group_iam_roles : r => [ + for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null + ] + } + iam = { + for role in distinct(concat(keys(var.iam), keys(local.group_iam))) : + role => concat( + try(var.iam[role], []), + try(local.group_iam[role], []) + ) + } +} + +resource "google_folder_iam_binding" "authoritative" { + for_each = local.iam + folder = local.folder.name + role = each.key + members = each.value +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/modules/folder/logging.tf b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/logging.tf new file mode 100644 index 0000000..d6a195e --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/logging.tf @@ -0,0 +1,91 @@ +/** + * 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 + * + * 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 Log sinks and supporting resources. + +locals { + sink_bindings = { + for type in ["bigquery", "pubsub", "logging", "storage"] : + type => { + for name, sink in var.logging_sinks : + name => sink + if sink.type == type + } + } +} + +resource "google_logging_folder_sink" "sink" { + for_each = var.logging_sinks + name = each.key + #description = "${each.key} (Terraform-managed)." + folder = local.folder.name + destination = "${each.value.type}.googleapis.com/${each.value.destination}" + filter = each.value.filter + include_children = each.value.include_children + + dynamic "exclusions" { + for_each = each.value.exclusions + iterator = exclusion + content { + name = exclusion.key + filter = exclusion.value + } + } + + depends_on = [ + google_folder_iam_binding.authoritative + ] +} + +resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { + for_each = local.sink_bindings["storage"] + bucket = each.value.destination + role = "roles/storage.objectCreator" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" { + for_each = local.sink_bindings["bigquery"] + project = split("/", each.value.destination)[1] + dataset_id = split("/", each.value.destination)[3] + role = "roles/bigquery.dataEditor" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" { + for_each = local.sink_bindings["pubsub"] + project = split("/", each.value.destination)[1] + topic = split("/", each.value.destination)[3] + role = "roles/pubsub.publisher" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_project_iam_member" "bucket-sinks-binding" { + for_each = local.sink_bindings["logging"] + project = split("/", each.value.destination)[1] + role = "roles/logging.bucketWriter" + member = google_logging_folder_sink.sink[each.key].writer_identity + # TODO(jccb): use a condition to limit writer-identity only to this + # bucket +} + +resource "google_logging_folder_exclusion" "logging-exclusion" { + for_each = var.logging_exclusions + name = each.key + folder = local.folder.name + description = "${each.key} (Terraform-managed)." + filter = each.value +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/modules/folder/main.tf b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/main.tf new file mode 100644 index 0000000..5d285d2 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/main.tf @@ -0,0 +1,43 @@ +/** + * 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 + * + * 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 { + folder = ( + var.folder_create + ? try(google_folder.folder.0, null) + : try(data.google_folder.folder.0, null) + ) +} + +data "google_folder" "folder" { + count = var.folder_create ? 0 : 1 + folder = var.id +} + +resource "google_folder" "folder" { + count = var.folder_create ? 1 : 0 + display_name = var.name + parent = var.parent +} + +resource "google_essential_contacts_contact" "contact" { + provider = google-beta + for_each = var.contacts + parent = local.folder.name + email = each.key + language_tag = "en" + notification_category_subscriptions = each.value +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/modules/folder/organization-policies.tf b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/organization-policies.tf new file mode 100644 index 0000000..177a3d8 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/organization-policies.tf @@ -0,0 +1,90 @@ +/** + * 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 + * + * 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 Folder-level organization policies. + +resource "google_folder_organization_policy" "boolean" { + for_each = var.policy_boolean + folder = local.folder.name + constraint = each.key + + dynamic "boolean_policy" { + for_each = each.value == null ? [] : [each.value] + iterator = policy + content { + enforced = policy.value + } + } + + dynamic "restore_policy" { + for_each = each.value == null ? [""] : [] + content { + default = true + } + } +} + +resource "google_folder_organization_policy" "list" { + for_each = var.policy_list + folder = local.folder.name + constraint = each.key + + dynamic "list_policy" { + for_each = each.value.status == null ? [] : [each.value] + iterator = policy + content { + inherit_from_parent = policy.value.inherit_from_parent + suggested_value = policy.value.suggested_value + dynamic "allow" { + for_each = policy.value.status ? [""] : [] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + dynamic "deny" { + for_each = policy.value.status ? [] : [""] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + } + } + + dynamic "restore_policy" { + for_each = each.value.status == null ? [true] : [] + content { + default = true + } + } +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/modules/folder/outputs.tf b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/outputs.tf new file mode 100644 index 0000000..37babc6 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/outputs.tf @@ -0,0 +1,51 @@ +/** + * 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 + * + * 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. + */ +output "firewall_policies" { + description = "Map of firewall policy resources created in this folder." + value = { for k, v in google_compute_firewall_policy.policy : k => v } +} + +output "firewall_policy_id" { + description = "Map of firewall policy ids created in this folder." + value = { for k, v in google_compute_firewall_policy.policy : k => v.id } +} + +output "folder" { + description = "Folder resource." + value = local.folder +} + +output "id" { + description = "Folder id." + value = local.folder.name + depends_on = [ + google_folder_iam_binding.authoritative, + google_folder_organization_policy.boolean, + google_folder_organization_policy.list + ] +} + +output "name" { + description = "Folder name." + value = local.folder.display_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 + } +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/modules/folder/tags.tf b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/tags.tf new file mode 100644 index 0000000..2cd2f2f --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/tags.tf @@ -0,0 +1,21 @@ +/** + * 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 + * + * 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. + */ + +resource "google_tags_tag_binding" "binding" { + for_each = coalesce(var.tag_bindings, {}) + parent = "//cloudresourcemanager.googleapis.com/${local.folder.id}" + tag_value = each.value +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/modules/folder/variables.tf b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/variables.tf new file mode 100644 index 0000000..a3f32e3 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/variables.tf @@ -0,0 +1,151 @@ +/** + * 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 + * + * 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. + */ + +variable "contacts" { + description = "List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES." + type = map(list(string)) + default = {} + nullable = false +} + +variable "firewall_policies" { + description = "Hierarchical firewall policies created in this folder." + type = map(map(object({ + action = string + description = string + direction = string + logging = bool + ports = map(list(string)) + priority = number + ranges = list(string) + target_resources = list(string) + target_service_accounts = list(string) + }))) + default = {} + nullable = false +} + +variable "firewall_policy_association" { + description = "The hierarchical firewall policy to associate to this folder. Must be either a key in the `firewall_policies` map or the id of a policy defined somewhere else." + type = map(string) + default = {} + nullable = false +} + +variable "firewall_policy_factory" { + description = "Configuration for the firewall policy factory." + type = object({ + cidr_file = string + policy_name = string + rules_file = string + }) + default = null +} + +variable "folder_create" { + description = "Create folder. When set to false, uses id to reference an existing folder." + type = bool + default = true +} + +variable "group_iam" { + description = "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." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam" { + description = "IAM bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "id" { + description = "Folder ID in case you use folder_create=false." + type = string + default = null +} + +variable "logging_exclusions" { + description = "Logging exclusions for this folder in the form {NAME -> FILTER}." + type = map(string) + default = {} + nullable = false +} + +variable "logging_sinks" { + description = "Logging sinks to create for this folder." + type = map(object({ + destination = string + type = string + filter = string + include_children = bool + # TODO exclusions also support description and disabled + exclusions = map(string) + })) + validation { + condition = alltrue([ + for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } + default = {} + nullable = false +} + +variable "name" { + description = "Folder name." + type = string + default = null +} + +variable "parent" { + description = "Parent in folders/folder_id or organizations/org_id format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "policy_boolean" { + description = "Map of boolean org policies and enforcement value, set value to null for policy restore." + type = map(bool) + default = {} + nullable = false +} + +variable "policy_list" { + description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." + type = map(object({ + inherit_from_parent = bool + suggested_value = string + status = bool + values = list(string) + })) + default = {} + nullable = false +} + +variable "tag_bindings" { + description = "Tag bindings for this folder, in key => tag value id format." + type = map(string) + default = null +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/modules/folder/versions.tf b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/versions.tf new file mode 100644 index 0000000..e72a780 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/modules/folder/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.1.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/terraform-cloud/folder-factory/outputs.tf b/examples/guardrails/terraform-cloud/folder-factory/outputs.tf new file mode 100644 index 0000000..a84b9d5 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/outputs.tf @@ -0,0 +1,20 @@ +/** + * 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 + * + * 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. + */ + +output "folders" { + description = "Created folders." + value = module.folder +} \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/folder-factory/provider.tf b/examples/guardrails/terraform-cloud/folder-factory/provider.tf new file mode 100644 index 0000000..73deeb9 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/provider.tf @@ -0,0 +1,31 @@ +/** + * 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 + * + * 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. + */ + + +module "tfe_oidc" { + source = "./tfc-oidc" + + impersonate_service_account_email = var.impersonate_service_account_email +} + +provider "google" { + credentials = module.tfe_oidc.credentials +} + + +provider "google-beta" { + credentials = module.tfe_oidc.credentials +} \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/README.md b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/README.md new file mode 100644 index 0000000..4bb282c --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/README.md @@ -0,0 +1,115 @@ +# Configuring workload identity federation for Terraform Cloud/Enterprise workflow + +The most common way to use Terraform Cloud for GCP deployments is to store a GCP Service Account Key as a part of TFE Workflow configuration, as we all know there are security risks due to the fact that keys are long term credentials that could be compromised. + +Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. + +This blueprint shows how to set up [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) between [Terraform Cloud/Enterprise](https://developer.hashicorp.com/terraform/enterprise) instance and Google Cloud. This will be possible by configuring workload identity federation to trust oidc tokens generated for a specific workflow in a Terraform Enterprise organization. + +The following diagram illustrates how the VM will get a short-lived access token and use it to access a resource: + + ![Sequence diagram](diagram.png) + +## Running the blueprint + +### Create Terraform Enterprise Workflow +If you don't have an existing Terraform Enterprise organization you can sign up for a [free trial](https://app.terraform.io/public/signup/account) account. + +Create a new Workspace for a `CLI-driven workflow` (Identity Federation will work for any workflow type, but for simplicity of the blueprint we use CLI driven workflow). + +Note workspace name and id (id starts with `ws-`), we will use them on a later stage. + +Go to the organization settings and note the org name and id (id starts with `org-`). + +### Deploy GCP Workload Identity Pool Provider for Terraform Enterprise + +> **_NOTE:_** This is a preparation part and should be executed on behalf of a user with enough permissions. + +Required permissions when new project is created: + - Project Creator on the parent folder/org. + + Required permissions when an existing project is used: + - Workload Identity Admin on the project level + - Project IAM Admin on the project level + +Fill out required variables, use TFE Org and Workspace IDs from the previous steps (IDs are not the names). +```bash +cd gcp-workload-identity-provider + +mv terraform.auto.tfvars.template terraform.auto.tfvars + +vi terraform.auto.tfvars +``` + +Authenticate using application default credentials, execute terraform code and deploy resources +``` +gcloud auth application-default login + +terraform init + +terraform apply +``` + +As a result a set of outputs will be provided (your values will be different), note the output since we will use it on the next steps. + +``` +impersonate_service_account_email = "sa-tfe@fe-test-oidc.iam.gserviceaccount.com" +project_id = "tfe-test-oidc" +workload_identity_audience = "//iam.googleapis.com/projects/476538149566/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" +workload_identity_pool_provider_id = "projects/476538149566/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" +``` + +### Configure OIDC provider for your TFE Workflow + +To enable OIDC for a TFE workflow it's enough to setup an environment variable `TFC_WORKLOAD_IDENTITY_AUDIENCE`. + +Go the the Workflow -> Variables and add a new variable `TFC_WORKLOAD_IDENTITY_AUDIENCE` equal to the value of `workload_identity_audience` output, in our example it's: + +``` +TFC_WORKLOAD_IDENTITY_AUDIENCE = "//iam.googleapis.com/projects/476538149566/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" +``` + +At that point we setup GCP Identity Federation to trust TFE generated OIDC tokens, so the TFE workflow can use the token to impersonate a GCP Service Account. + +## Testing the blueprint + +In order to test the setup we will deploy a GCS bucket from TFE Workflow using OIDC token for Service Account Impersonation. + +### Configure backend and variables + +First, we need to configure TFE Remote backend for our testing terraform code, use TFE Organization name and workspace name (names are not the same as ids) + +``` +cd ../tfc-workflow-using-wif + +mv backend.tf.template backend.tf + + +vi backend.tf + +``` + +Fill out variables based on the output from the preparation steps: + +``` +mv terraform.auto.tfvars.template terraform.auto.tfvars + +vi terraform.auto.tfvars + +``` + +### Authenticate terraform for triggering CLI-driven workflow + +Follow this [documentation](https://learn.hashicorp.com/tutorials/terraform/cloud-login) to login ti terraform cloud from the CLI. + +### Trigger the workflow + +``` +terraform init + +terraform apply +``` + +As a result we have a successfully deployed GCS bucket from Terraform Enterprise workflow using Workload Identity Federation. + +Once done testing, you can clean up resources by running `terraform destroy` first in the `tfc-workflow-using-wif` and then `gcp-workload-identity-provider` folders. diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/diagram.png b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..d4e6f82e9d0bfbc41c85ed9e3e5b4aaa5b72ac25 GIT binary patch literal 29084 zcmbrmbyQVR*EhNWrIk(z0Vzr86hyj9x|QzkP`W{+1*Ac`8>AzgXK~d0DX+$b`rc1ig?D7gmHIICuzxd4~uKo{;^T1LrN#51z{|6Y3ayq{^2(pX+|MsqTz(@hYA?a zabl@ne{!2$FxqG&r6LQ;P{Uih*4F7}`I*9B(NM~ntLTR=h)Cv#ZZ>VAsS1IA2z6aB zV?I41>c0_sdZktT=?AiB_`y#Peh>x5f1cL+pI2eSgr9y%$S3+g4~YMLFN`Spf8O=y z|I?eH*Jd6HsjF%eX1mA~udt-vU^|k2XB#~AL)X-OA%vJNh9xtKEiyl>eefD{n~j~X zbNYrbIy|@?lNX`WT>!1OoTxbkTpT|>X0V@r0&l(4>f;=BLLfSHL}b4E_BFVYOVS9+ z|LeE$`#-td|NUK)^xhD@H|tyQyzR$D89c2nMtS-O4df8?k=pNBIT?3k%3ag*SWpHz z#b$pZzg@dcnVLr~@AUiEJSmLSCBGIvxW~Tzvv|26_pljfiSUv%e0xfNW0cjxGri`+ zXk}ByZc|#PL77c$G&2YS!bkGxIa4o_km$T>$q!$@R-`4fA0H!0W`EDIock3UuX{Ba zp$*9^Yk3sq<=N-TLPPbR{sfWv9^Ut%=z6ZMUM`%RUsP9ARO}lr;Ql8)GK-nG!OuuY zNGejVTL@Tlc)59I`0!!=e5DX`KIFd|Hzu9nH(|#2$-uUAo%bCdXOD<@i7f)-qf>$x z+@To!3>qUWQfsHdMoynfY`1SI);Jw4&#rP_Rdm+_) z4X$A8#lHh>o9^d6hBPJKyg;NELcwi*n9wdg10x7u1T0N_IBdR7QX50n zQ37i#FW;1dG>x5|ebM8#r8i#5OZ_2JC>=XgL}<5H`?I==gTP1|Q%gyi(#-wC=_OU> zvA^nq#bULLunRs^85*H+H$$G30SS`F-jx~DNmJckbfz^p@3VNFiKobN+Ap+v*)|z` z=1~|_YAMpVT`i}CZ0^RkDLNk>9(I|C_dfAn?%i&kQVQ)xtCZ++9bQN5&my4_5|C7z z{GA&qKiR}#n|LWa#C^MU^We zG$Sku`2;p3S5(+vP7Sb$(5~4DkRI+3p6{E7fnrX7#g?p+QIAO0-Q6t~L85)rUv8Z0 zW!GAJT=mh(N~UD;aMpV%eo^ITi_KpPjzxEwKBJ26FZB+zl&g}s5Y1YMpm&Srk45LB zu73=g?w4Cs(Lw5QO=Ui!1vn?@&=A}HmR`yDjvtq!5}U|qp8%d$WWN!pUU{hrdWvYD z)!M?sLe2%Z+lE8$pYJVq#=2wp`1lA3jg+;uD=a%BVhdDqvvW1+<3!Qkw!8=y&L~%U zNicXB_ zNHDWnM|^EmF(`*H%Z{oPXY_r*1==%Ie{dxSDy$L0kRakRSMjgXqY9?xyY(8J$Vk%5 z_OR`*Fffh?iSco94|T4+NshaA_I3;^yZ6W+VM8(kUb^k&E_$W0o<8n<`d#(vfr!UM zui)38fS!iB+6T{*N-Zj><{#fjcVVplOR? z6E@7?_P>8g8i+B3^8C&1b}`H7&8svbNq8UT(F9UMB1=@8nr%J3Ze6ea*Fss-+?yLS zR;VUFDxkLYrLf-C8K+%)j_)k07BMm~SZ)y(^h%Mo;_w?-R54-3%^fEz_%~9+z&4hqu{m z+f?7-RHwVkd)I7M6dKz5tMf+c&B{MM9RJB;Q#Ork#qHf)iWo`SkdYk+@_=Z`Y-eps z-@oFBCCYCPdvQSWPETkGrQTo$3zH9jhP&xJ-%d$zI0#Jh;;}gzk`W_c{4r$EURG%1 z+qB4U*u{akY+qjJUJYlR-7Z*nEAdZO->vMC9{G`%7X{mO-)T_Zujdy#`xXQuC8ycZm;J*6cMVp6a}qw30bws=^Bh& z&)>epKZExdDjwZkx4S+f^UcW2e3+>o?eDKry|i+2a!xdjNaM;M-ToIzeE(Vp-4EUE zjIB=mOs-0pJX(CLNpmtmmRMP@Va;QA;uiW%RS6{=h^KV=ma%Lnto6vgW?ET#GRIhqi1 zKZip?O(mt(<>6Y!8quX-=Amtl6x(AY$$aNhC+_pPs<3H!3q9#=0 z*1017%prp0KzSiG;Q>vi1P=-N`GrLLf3dzdTIk6Msk zH7k;lt?Ys`BqU6e`Fld?hPCl9v~gBAB!~Z}o*=eXAN8|ezc9V{WsN$9->Tp9>2oA2 zv9f@`)|O>9^6{H@qJdl{_GMm335h$(^jDd8uq4vc47E5($VIH4oS-=Hqy&PaL37`f zozPOps~MyPG+9PL{+<9Sw9PabMI-(Xwik@nbPE^_ zh)1}3bblOh5Yez1aK6UFmBoCWct%Ah(FfyB@MxiRKRIPkwy}xR^JOG2$R^!WJa-fi zTL-IIO5sOJ%1GP%{?YI%0vhRO^|%ZgIp@pmQ7TtgH|0;CO0-VJmaEMDsM$U`M{Kj{ z&zS_|tYQsV%{TX2aE!98Z*BcjF<;QUbK9pN;rB9l{MsfO6P)pyp3R}B{n*J#a=9r! zHc!Z+fD=me4p_A-+_e(4&V$~-qnj%0nAaH-GX|kg`UvrZ2;^KF!xk$t1 z9_f(4z+|WK?ZCWhd3gun&Jq9WPfRKOZ5(tyS$c);7aK*z3))Z^UdmLXgwcN)= z+5EF8o9oEuskG*F?7`O5#WMT31E)v(?6vS3sw|vK!^@GKc>#BGk)M}$XWv^rzB|#ve0eS7ZOOzym;BDLJhDc$b-EocFyhM1y~=9dY&5PY z{qJ82G1T#G2lL0PQ9b5~2%X;E!NrP0!7svBpYLxs*6}PJ%x{mea&Io5IuOh+(M($# zn<()&HR7A=>#?^lNvsx&E#n7LzOYQEw~0j(3oP&KSyXAL$Km{YdrV|qrs0|@K#(S{ z{)ye`EJ)?+SCNj6S?7ay^O5J`Pi@$QBRY>Uh-V!)X%!U4#upP`)q!~_2 z(dH}WJM?ww+ttl=cfVsS-mJE4DE?xUn{9lyJx)kgAeK?3h^Qa&H1q{3O7l`c&cx;yHAGnW4@g@=cW zN3z{ukIS5qM&$+Mr-0##s80kzZx)1Ckj5Mjb&_EH{Z2{|RB>nvVIvUsU$h+?Dw-+_ zUv2xff9{l%{mBd}jhDA!Xla6|69-#Isem0rKiw=`g&e1M-A?}lLTn1LoJ-S(!SceC z6vc!Q+Gz1LySi=*jwET+wmfCYkPQ3)#p&sBPp*pTzVf{shS}COpm`=6nl)DU$AO2|^LLwt#%VYk=;I89iuwP- zi7Twme_OX)y9qu%SXo((XgYoRMJ2WdLD;$JVFZ7+VIZ1%9TH46HN_k<91RExfRAl_ zD4@p-zVq;`Z+DR9an>J=+yC@w9QtFc!e`Y)W|^q+YNA7E*5MVeqdnj|?nI=Y?& z6TsbEq9DCg$lntt$Dm5z2%?!)NOD;U+v%Z@xN#2o;YlE=<{6ry?p)T~Bp*g0pF?JB z|ECWYn(EH8DM|j>KQM3rh6_PJupC#i!$G4ESxtFi_ZK@SyCfdlZ(vGkF-l3uXtvm% z4zE(9!uzTeyy7=Y@bxtVB}g}UcRc;w`ezDJ2~KQnEtkNAfNZ|9!`_h9?Ersp@TLr| z(x*={^B2i^K}Sx*ma^18`@8!q5*WtKs)Xrn9UXCQ2Jiz&I9!%4QruNb=9l*OxA)ev zRLoD1k%hi#N>O40diG~=k&}zhU++gi+nBacb*XQ@R~}ARaj{9zySwAGG5eucY1nVu?Nnuu*J+X95F8wC(nX6G zs+cQCba$-v^7t;a^*4Fmnox_0g=7HG(IG|adi~t#EpNp7OOp3D-GPT$Sy{BPA>e!! z(;nQE3@yK+GG6o17k-lWk)|r(on8FGvbnLoxwf%$4qvB*i@$WZxmi$NA=O)(5!LJ2 z*u<48C)c7Vuhw=A>qtQ^BN9PBPb7H-jCf zK_p=NTjtA`{peg{=`z)PEBgEi6V*Z;&c|!x!%27yAeER86z2MBR@FZaZV;FM4Iud` zs@c3LI8nd&+mMj_`jAUsIN)W-ch~a}x;I0t*hXi+h;eH8=ms?za8uEdeK3OC9SJdz z$W#hq4Y@;d-Wb^Kqu~sm8ddm1`X-{}7)sdL6DHM`i{s6nvr8%;utkclKY#wPT*axY zEj)@1Asz~bcC^~^K^UPOsBLpo%H8FK<2{(K*+hcd4FYm#LonTX{7%5hSGj})>2b$X zKO{K73kG+Pr*4XnW5|p?H4T+i>ad`6T4W#009>%-q)urpwrnR|QYW_^+?u@l>m6lf zBjg0mvl^0To6@RNOP3~?`Vt*Stv@L%0|f|$K~Xg3cxd>uy%E1sa*#`Ei9$h2%FmsJ z4ih$5pS#8Cw0dK7bRjddF>o4P&kOx|UKJG;9&X-I$M;&?5u+xR;CZyiMDHd1*f~pK zO^LHV8CjvRs%G>v-V+W~VL>$Ok1tJfXrhAiA=NEhRAM+9-s$}T$3uXyXS7~4lbrH9QRuNJ2PugcJTcaCvT6NJu35Hr=mvc2XH{eZ^YOei!xnLdVv9q>x)W$^TrP*|m zeQj@X&o}Q)o}@4=8GAM1{_FkT@+r1RT`d>dE0!)NIaDC}+`>KnY+^MEtt)KQy2>5j z_VWdwBGe`gMxKn!eN}Ws8Xr)ifWo<^D4A4KGr2ydyzH_mORW58Wp}W{YuiCXQLU{RQnaRC?-$|~M=b8& z{Er+~VL@5GtaStFE_P%6V?WbQs+VY2E?_q_#Hq5B(&p`)Te&lc7SE4D`5b1olj)3m zI9G9byWSG3s>082X}9>k$&AgcN<-WIf$<%&<@wr^_S~-u&=>$&&9?(9OEkmhTSloM{vdbad2h7}439SJt0)<*U;9$`K^oC-G@scMlDZFSZtSAJA2I^M_3sv$Mt@SQDi= z8FG|qXhS3=WJXOKec{hS<+vKm4P8C!6Hk?=Lh7V~fOES1NGv8d5VZ<9B# z^07MU)=nDlV6CkWApuX-6a8Nn6=Y!Qj| z5MVEK+J@_3p@8L4&k8#7_T8dUo$_aXql&uUzu)q&_ULNX94u7S{H}L9*xNJ~;Ore8 z9i8}O{sZV4KP9dC_me6tCkByZj^p?I?&lG!>sI*g3P(HP4y)G~9;pL1(! zIE!ESK1K4MJGMq}Qp4{Zvno+6Y;?Wd5H{rMCr8HYM#F?o!Jaw{q1=(-={Ip-xW|J> zGQa=3VFODf<(EW3m+q=H0$$U=$9REygm!dS-7M&LkdSEH_v{rHU{Qt&5Opr8-ka7z z9I!?a$^Pr({2#hJ@=_`q3Gpdz%herf6B-J3{S&bwh-7Daz)OlGtQh`dTc}X7?n(l2 zMHm+0#(a=-A%WRBGQr%j-5=qnKgs-|Z9MKZ(5lL{rqF6^GXgo8;t7WQ!*^57qvwNCRlD%d!` z3+RMq|E(G3s9@s-Q7R@G%{Xj-($=w~`2TE3o)i$H#Fu6()4LCOcv{G zbJ$>uC>Tc`uy3)c#}C$Qk0_ZQsyQ*}YAO+9&eKvJV{bftfy$>q5QSvWAVYgZKc(TD zl#9`WDWPAJd7mN%gfL<=D?a2#)9pcjevPX2^; z_sGDfXyuPgDBID~#nM@LN<0k@E+U<965yw0ze#QU?onM|C~UHUkw=uWF4+)@)H%ZR z2LF6!##&+>Sye7=O)0QKae$h5anROcx63IW^I<@?DJsc9gFGfQn!n{@amfAR492F96 zimTEvznuq*m)`?9DQ47y0tLvK0s049cT=_jBp)ynwa8;oY6wK1IeO8UeGaeA+0i(_ zL0f#iES25#d7f2#J*58L&@Q`YViHuq)?Z+BlCbaBcd+LqC{v*!Tko!InC{*HmBtK{ z88clhJTuh&bI7=*#8buDxUfGxp#RtA{C}9BHZJGd?$_s>XKubl=3mi;YSQIrni|f; zG$&!xjUb9)?UCIVi%hpO6IG>Ob6S)sgz}12jbthuanQ|3j_jX<^9mvU*Jl0y<)i*@ zWAgvSgcUq{Eo+8Bi^yEvT!}D1uQ*+gjD`$DzK)8$Y^ja$6brJt$LOF;VO&xD-CZ~Q z!mzQ4PcA+*uN76ScQu|y8d^8p7FQ;XrY`nV6oyj%mi>=)p7(S;Ik5Y{moiINEBGX-9;=9gW*TFMS0BE zrq+Q8oDXQ96da#}2n@+Qy@EYxOJuw&`71D@O2bn7xt~0ie_Me9ivPSJ16K1MVtN2u zunKuoPEI9rO?hS4m!ae^cumz@1&t~*N4-%=&B>LEFLwT|vg>=x`|FF60Vz+HEDvWv z6f7<3xzUl8bb=}1-LlnX<1a)O-)njQlTR^BG*dR0?k(ecdzbj+c->tNVoJrtB07JN z9(L~PFyZla%6QC(=g*%zS_$~*lic$UTL>}61IdGvv!*L3=Lt7a_QM3yz_xf70=bLkl&w(2O z?tj1il{R;l2Ca9@V#?^B`bKuvuV1hGK5{JYgJa#1{rp=I-H62);9z0AT_2$+9n{Xz zYqO@Jf(YPe4qKoHpCu(Fg?x`%($my+@5Lb@QE_4z%$o$0tRIqlUGi;@ZxAku_oil( zJWWZKYheP<2fWV~yiXG4)WhT6hOg>HaEF1lb=voDTAHd^1T+dUzVX+D{4jRLk7f>Q zHpseKzP(GYfeE4KE3T~W*5BJUKY_O};tC{-IP95vT$z+a|1fepzVJ++xsH~W(YiBo zgK~qnpz|61fsgJo8JeA5PWsA`qnID&mQrLsJcgt4-4l2>I}M-#(q6J$Q3 zJQ&M_=IF`_;^$Cg=l4w0yT83!`8V}{Kl=UY@USC`bgs!I#^$<2CG$&SdnF8*7cH^? ziA7ijO=@R)%L{Tywf71xo{R2M_ci7_^exdB=1d_-8K3MV8Ly4o=t+&uER2oxUh%w( zU-V4vrQIxgw4yOzUs^Kcm{OX1zr?$;9JswTG?~LD zib_hmB*7seKqYXDw3w;Pmr3SWUS4+AfB~d?qmKaEnyagjc0xem*Q&K4qo+rmED=MT zMz*>Q8It*7E-r){uQ@QDBAgQ?I4GavjlH@o-rW(UVxoRz-tkSBPVt0+OsM(F8iRg&YUoEtJ>JMVczMaAK8shO9ThtKck0ABO(xVt)DrK3CiHFr%StiL^21ar1t6(ME++y;WGmZ>Cyfr*r(pP74**95$Q14icwM!L{BuGXd+Hm z3H=#(^RYBujWK-cOhmkOZ4V4o86gQ@5G(`)9H&!p!Zg4tWXu7z#kmEaU z&yt8q^|~>QSgp30R#aE#wp$a||Jt7^7O7M3kS`WVLd@&bH!xtN&OuKvojLr5n)-5Q z+))?tD74}HL!a} zrBID*4)QUdEZ(0g`wT*V`Iw@|-9MCB&f;Oyv2a?CogT{>M;he4FW8+} z5!7$h=Vjl)`fV1$r&hq;*$3FY2p(LsvaQx>a%;N3m^C#uMfNwiz1WA2RzjInl$D>s z!@s{?iviOAe7)oDWJzRjuzJ12b}$z8%Iaz)5jQN9ot-Uke})BlfEDF_dEjVg$K!gk z24bYGr3Gxs`1tsgl=*^^W=X{E{{H*@nngm5y9}_xwsX^fN;5VqWs79+}Q)2-#Fz=E~vaW*UlDu>kTN?%@H-oRjGtvd$hPg~n+ zt3S#sPOH0%S?l||ySw}Qf`S5Tx9`piu3~R64ZnPW1YKNeC9};Yi&JuQjn&kq>g++x zcsUK)g96@`I_^#kXNsW_vLQk*iMXO-W1mCGoYuv^e{0m)QOb-KsFtL@_vDco<##)q zsaM~wv-z)pz3twfu_%LmpR>Uu-r?-^EZmv?IbKmlKUm}q1N^@h;O@r zZQ6TJm0~S!m!qZ9($eV)W4J#@%WY^ReE8VdW1wDV$b>;E+)uixS65ct&bG+>1l--- z(VmXT_1^g1b2rci*$6?(8|>C4g%Dp#3p;;69L3MK49&>Pxi}4tMx5@uyFNX?39R4r z?=XP(IYr7QorLAbUeb1_K!w z7=Ub7X%zYT!a-O6IY9ee{&&iD)@yjSHSB(|SLd)jQf}Bw`1_-bjEuZ|l+En$utM%A zj3B5SkVF}o@PYz!@DEW@Q5_wUHfGSc*a^d1+lJY+oqu{cIf0#9r?0FVk9+7!=+#Q~ zfDk`EK2G0@JjnuLRDVtc(!qP!I0|Y_AyX8{f(7Hq^EL)$wbqNxpf1LD&s*n7z3*(V zYL<7xITz&L#@5u5`w_O<5|tVm`zJu6a4H6EX4)R~lrFlPWudlCf zY#4zJ9G;#cJe0uiUfumg17ro%J6K?^ zh=};C=4u-o`BFEP6cw%9Qax`kKB=fM>NKhW_m7u1xqqYDa@NSm2z2y6ddynQ9(7jw zq@<*w9p9s(jzAj5NWvl_-rKIU|N7u-XJ^M@HOB@1W)5@NZ%Ef^d7W)3BuaBQAIz1s zJh*}D2lGO;U_2rs!uc3ih#_&@0K;B17B;@K;vJ5l`iJbM%$Op&6Vfo@yzHx`GE9M+!EO%wY9Y#cmlr*07=z8B% zMvE_aUTOo3U8#Z8=4K{$wO29dbun%H`Ck}}Pghr0cMNr?a0b8`&Hy3U)!kihrj@9A zfy~#{)wLp0tE#G&sXZkQ^md6Ds&}ZT1H6aN5D*3v-hBo^ejEGScrex0*7pA99L&Ka zHF^?uGeEr^@VHVba8H}k5axLc#tnAgY6Y4(7{3J!)moZ60{2x%P12-2UMAH)! zMVh3q9)&+H;@FGQy2t1+B<1XQlXJ)`DQH$4D{=3iOx`aUDZMs0>hEsM|`Gd7gc=Ln!&Omw(f$dAnJIm4c zeW!OnXqCL~j>S;XAse!Wsqy*Mg~8FVMi%p#O@X2^08P=Gbi@+b(!VxW7bAW{nHDKN zNDXHy`NhrM&rx0~OkOuSuWb8|K!_7#>E9VD9xu(Wg@+4xR_^Zx;|Y1T^UBDy=Ywt{ zie?t7yhYIh9ttI_w&w>cd0Fm$625bm6@z6e1;lJ7Lq0HY%xHke73p_^bEQWKzS7OH zHy{2hHCeT5$Jbr*x?xarh1^i?Ja_O5A$iusEY9_mj8(7LAymd}@=`5E!C_JiW5Bs~8bW(d}9oOR;~KH}IDQVphMp zLj5S&obTkIz`$gzFo@`%8)KOi^u(`70PnMsefy2sI>$J{rI&!UQ#anRkVvb~__X(0 z#fopauFya#Hlo2+jpU;eKgEybX2nE&_q^e@oKKS;7ZXygtym>}NmS9|y7xB0J%5KM z&V}-}TNlR=*}EhpIs$?;U5Buv+#Y?R%-~{OVg+Ihi$}@@uU_?5USK<;#5ClU1|v@U z^!zO|KlHL9NrjeHcEEm7fj%(GHA2lP>z($Kv>2V8oSH9{__-)4DN);&9%MME4e|sz zbF6=4m40JY-{pi=fUqgxg?-p6u`MaFDZdY$eYC7K1sjM8#^Ikm8{?g7a=%RB zbzbN-1+DWM4&h}OKrX#ZEsd9wPl)$?XB+@3oaVjRni7=)8EI+kW+GOjK7d$t(KD$2c%Xs!D>9v-qA6yO{rR>QRQ~Nb9?)PBj7`c`xV+u`!Kc(4-4FkWXe9F z#a=qWJBIHHXWqnL2HxJ3y}VdpG^h{WU-H#Rs{PQG&7!{em*U`FnZXZOzZylCUL+2Y zENd7Y`)nk08<>pss#iIkTG4) z^YK_ZRnTL{@|DEG@Q)`nt+&r_A_X2?9`3Hce*O9cOYQ8+8yapynVRtl2omGtYXGzY z7#?uDnnf@7$A^1^G8X_e$;ik;aT#vzP6zGn?RTqg0Eqxcs(^Ph;17XlqzB^#@xtWX z4qNg7vNLJbE^TgZuC9J&NsMFAbXsXg5)

w%=&~_s@(isigS^wDyk9P9`wMxNtgc z(>zWjIwOcjM@EoQP`s~}1M4lTr>Cc-;^=7sIYj$U_HmFqzzk@1*j9`duLKK+PPMr4 zs1*e|I-k@9xQvUB?~XBbYLCto!TBLJ*>gSS(5vP=bZ)JSQu$V}yHwW0uDHX(GLJ#K z-5zyN0}2uruCk^)9Fi=K;usd-mR~*QTP)<2$9DRih_Wa@mG3vNokSawL85^mInRF^ z^#VhPFMcDSr8ZR0u#^i=4>0_A!2^F%HO|tM&$%j zK&ZBcGy9EBI{_RTkz!&{FaI)8sP2n^_OMe>0(K;UD!Dz|UfsQ2Ov(j=Bv&r&V5Vv} z{bOLjOX9bf!74=>?DiXdG1PKTTnxgcSJPujY`#0`4GRkcn8R|qykma;9-N#=!0QeS zCBKvxE~$7NJTCX=lb;O?Cctb3>wIP(Z_Ml*;9fj>wWE_>CO}RO!35+6SPp>k@87>0 zeEzIjs1}`&z~y|P6=&E2C{`k?@xri@+0gFG!|6Deo!V}bcHL(+OBP}>2V|t9A}ZE9 z>YmRa#E(iWNF75{@5lB)cMmInn?-Iikl#hy_3Bp{$^(Z8wjr}UmF!s71%e1#k_^YX zrEK&TSdPi>J_p3aU_sKL1lD_ETj6I;HwO<#b0g(NYh@6x0hO4oGS3~|o|~J?QZyA2 z5fK+h6j1;wR7XcgJd^GNSU|$$3SfZ(YIm}k=6$g}D#jS!a(mE#Eka36O&8k}uj@qs z-JI{LYG}Mg-9kb|r3VJct6-3z3a5RI)J;Hn0Srichsx@HVFAR9ow0o5R9DfGN$dG~ zc7Fc(?rvH(wzIwI3cx_TwzK2uaeG0O!LFU2-d-fK(~bU7Gf=0+U<2a(y!IPRS2wqr z>H7-G<%K5qwzjr3_XBRo<*@0p+IlfHDvJ3_R}|p=;D-l*d@N_H`2l4Ez}&*3P-u9{ zN;P%!^yDO><>A}3#rN*@BkRWjx{4hBO1w(f^9fcqnrv(cJ_Wb^3&(UXt-F68Xn5Od z1=XU~m2E&nF5SQm4haqAw3sS&zqDHJojIO#yu7?PJ9`F&WDKP8-?n4PO-)Y&y40*Q z>a4Dwn46m`el}QjdwmTYc*rL?`PI2KOJ2%%DsVf?J_C%8euA6K7Qg$2$8iT-jyuVz$y*y+h+McaB{QaqboSYmR**`fsdD?=Ji3zV<7HlvmJ$9K#zzppG8TVWb z=fq{usI*(}N!B?B)5LGqFKTX13z&m1iFvSba4Z*_o1709Kx25K%3fX%<|R!|x#!>} zmiG@Iz+Wg51`72>#`x`vZV)r!u8bxdEQTg#e*?D#*5m!vYUJhNA^^irbql1q0Li!5 z$RQc!6&0+ktbqS_oi+}e6y96u>FLEYYHtjrxPvAHx)SaC_fN3ZS-ar9=Z(c&tpYyx zGYE{=*|N`p%#+fL@p3=8Y(eLdkdOdDnFWle()7F`n+BLeJ4H1MlCrW3jjpHRo1}a$ zHda;#pnP1xiB)x6L4Rrb+!=u_@&cb(uRn<$V1a|7^tO&rJWme~E$zdqWB7(0;5AmM z$uz}wjgMDxl!TEAEDQ`-0n*4f2quV4s~-}?Y`4}m6Hx@ZBT`LMV!1`D&8 zj=a&A$AF|0m_L9`seb@nY60zusrf47Q-KO-$frj~%!$%f#Bu`z0~Dfntj1M92#NFF zgaOe$`#1Ft5ag4SlbPWmVjhPCW__Xk`FMKua*yj1N~t&(cXzPt`T6*?GKUe_1HDHzY@s%q<)>s%TZ*t~NYf4<7l0a?#-7e(wz*692vFg)a( ze4<89OwAGFZn-p;Xa4g|WBjdWE$*1s5uZ10?pVisX7sxG?}EA5$_A+zoxNsCnvz1s zar+3_%T52GUB@31ScF^dqlZ8vf)m`wAL126JmNzq!H3V=7=s&Si&gy;zq8Y__2fr| zr@RLn9q~5?E|ko6ii8pQU$eDkZQ_3UvR-Nt`s%|cwW!TqJe$K!QyrQRc#XAXyepr9 zK_LA{&Dk+0SOe+b|21dN2bfxwkM^DB>{*3B3h1eM7~T;gb}C>ZWy_DelnrY@SYHv9 zcvD?WVMY&_qyP8rHJxS{s+eH5)2ge3AKfNg54R={I0ioI1m1WD2BXqASVg~OiUpT#W*Hm58 z=e2c7>;BT$k~CfMsHZu(KCDng?QJZ$dWsHZ=}>fz27eaospBph^auA#Zr05eZ1`bq ztOnLL5-D0QBBfF!5vnM)8D%9rzNbIHrrV#FBbxtO?d-$*a}w+q7639BAee|sW~l%L zHxwKKbuKL`YG}xrJK`1$b09R*h7baNQ!)5HIX?apRI?}OEzZYk01@@}_iI<0410P{ zkizaA3g;OH3pfSo3wNR&R5^-}5c7SVL!L_MgLyJJPWj?>C9ZJCM6bp5)O)j0ZXD@Q zcU8+03D)9nZH{*cPnSJ+#_1}sSj;NtQkusWy>1y~Mm2%PT&mv&hW5qbA`=m3a(`<= zu7+9Fj2`otmuZ=r-}!}=vUR2{bhRF@TdA?1Ba;k7%SbSkNci2x49b4Q#*Wkdv(<9F zE(*X-md$n3lm?rC%FxmUT|_yCMLXkXNLMmQcBTmPd0_jQhB1;lE8SKxN+hM*j7V$ z!NLG72A7iV18IfRat8P#K+%k&R~JH5ueC`BP8bst(C%(lBLz~El6b(IqM`slJ>1>B z1tcBCn2we|s*Y|UEu^?Gm+TkKM4VzmyLqlIRs0t*TY&yJ3kTp#)p zSs}0&R9ive7*H$3S1~PTBT}H(vL{NvjWq*mI#rD6(b8XE_!k-=*P7N%?0SEE04pOA z@M^rUkcwyY+(mc zi3%}BG(ViGi&almZ<$UW7?sV@i2ef6k_R5OGlakF!RTG) zQ!@Ekbacs7?%cYqU1Vs$2^$JK6pF|>4rH{=PTGY>!+^y@SrQ_#b%t=Pm}V>0N12ib z|6zqEVV=I;_zJrulA)qd?)CA_yxXYN!DDzT@PSyh?N|1WNAl?3{>Xlq#($k1CDoJe znPQsz&oQl|y`6~L?#ooEJ_;`V6i~N;Mw^_hxo@NbzC?3#GZ0TfUR$HTeS-zkH86NV zsDOHvy1AR$-`#Ec_uKQbL|LtL(Q@V`Af(91$Qm_P`-g{% zS8h8F_20gI3knM2t*`&_t=APnmbVMz>HR>kE2z}5zih0EE&j?uCA^!pcw$g8XvfIpcrv2b-=pk^v9^pm+~dU?3_1IgvL&$H2w_Fi}%m`$;ecY#=T!4i^s(<)c5O2e1XG zRLqqzpqSJdbRfq{#(w{P0wzXoRu-aRs@qQfa}-&%H^l1SOgUDh}AEbQDrdQ!SKcB?V+*T#NT_eEWhQ&V;f zMQNB^n<$+xoCXLwm&H?LD$08?R!-=3GcME5c^~ShASv4&*{8>jz%NdLA+sOw0=Un^ zhb@m_GaFcHlU)T}{r%yQk$5*Mgc7-N78f7?-WThX>U{um!>hX5(!VAnn!(bH z*3!$fnHC%{~H?{19pfZ;C>MLe_S8lh*jVUo0*y2T;4AtmKU!$3IW8gidvhZxFN4V+S=b22BV6(BYF(C*=%88Ot5rrNqrBfi?+kTx*>;LSB zflwiQt0b-Hn@T!0bvwQnxA5N7CF&I$UC|WC{wv+oX+Och1O~Ty9B@I+4Gq`x z6f*hv`GNWSQDH1wDt@Ngau*!i+v?idDPXY@5z(Wbl&aB#wy0Dv4tnVM#Ra?%Sm~5h zRR20U#);KoAztSLBH(ZUz+lFy+tb&_XR{<&pi%@Tjj)0;_z2)y02u>_95~3&J8i*l z(!71^wpZ2(0u1!GVy!xIk1Zf+n1jxBe0&T*yjTPg5;F1=Q2OhOAeYKl2TW7o(=KcW zPI%_36j+~L%OK*9NOaptxM13FO0#S_2z!Q%QYq}PRvt?CluW8pfZ9 zus^OD(V>VbsUvWYGx(LoFFwfe<}1J5qtF5VTq?i&1b>F|+rXE^8n^3D%HQ7fD9~0w zo;?8jhvXY%Giz!tfo@{uf$i>oJ0nT~MfI*bvc|jyj>ADb@ZDRjQmZWEoUt2{sfdQa zW!ud@2_V8yaF~T zH7l!9wxZ>HJr0Bm3|s|;<<-?wz{(T)H$WMA0i_0f4)7bi?~V~53&1=8ngOC2plNVE zcsefWSy^wGnU4URECQ}NAa0SW6 z1elOe3q&^H5LwQ!QBzYVUh*WHyZ7$#+Ed#kIXc?+2c7GR8+PZTN?jK7Fu-n747Jc`}@T3}L&YE6a1 zdwpl9*HwQY1EH@=HB9SXDkLlPS#w$Pr*v%hqkWP24dTwIRXYSG?33cdWj-bjOzNkA zYB6>$7M|8z{1TQ9kEl}WCV=pVWEqX}yip(sz4cgy0vZZK^jSfAvtA0lu)00!z|*yu zuRl+5UKlp30)#Ccq!5@)IkG9p5F;Zaa7!U(V1xl|v+Yn1+B-1MZ9qQ)A|!YT#s_RQ z3JXKk=Ql7mHZB;i2Rt4OL|_6P%r_h^w&rN z1whoxgL$xOU4dW(c2cF6{hgi$TXT1SECOIY7Rmacz+>U8fx>RxW-4eU+D1kjK)6kH z-5|(Uo&jQ84tg*>u9TVCPBewsQ}ELG>{ET9@??8OnbiCMi4HYZ?NZnYihi3JdE;sX_rm+aCl&;IiOge$2qj~N3>nHSDH1Y987i5H%&Aa@C=r>;JX6TD%~X<*DMKMs zM1}~Z>0G<-`K{kNXRY(!Z>`fmpIRTo-p~8}zMuQKuGe+%g;>Wljw~Wk{>TxiU~SVY zM823m(3Wd!Yq9dXscwabhhwL+D>oajX9&yuk3C$paGCwE>-lTf>N7Gjma{MzUuug^Fg|g=*7+=g zQi(c=5?fh`p}kDf1<7O=`x6(-Oz&JdPDk8vqqy(E#HNJ>3rl#uk80}Kj^9-c@&$wW z63c{pX6JRQti7*2jfn^sEPQ4rXLX8pZ}2oKq!ge1^g-v@lEYC+(hlz+lf#^c<{-in z(a@lz)5~~37ib=2ujQ4MeDiB>9!kJhe~J6W;jXuj&lzK5peHre)d9c0wnBN1fMpvR zx?A4E4(ZK>HqtM!xEemXGN;cksMJkXVpA-#Dww>%x0I^Q)=Y!KeJ6Yy+*`SM2m%`xtPZ|LqMsCq%51P1;^|Fuy*U};`+ zwdf>2qvkI{BPs)qV4_nz1V~HGkS)*oEtKP+wEDG`n^netCj|u?2o1ne=!_uui!y|f zwE1{T-!M7`W@hPYzr1f*Ax#R$W_*5L_)Z=3CjqkSaK7JaOL@$`#S;S6R^{5`PthaN zND^bapy$)vsdH{%q2~oh*t(*L!(8muoL`@M7j0B*X$i3gO6iVjLK}L{d;+&kQk_Sa z*uJRj@OvA5^q0%AwjYYc@PvO+)}Pcl%*CZX#zx#_m~lWgCNX^FW$dI~$!Y(B?w{x< zo9siQ!k?{`SK7rJ=!cpN36(ba)LM56Tv(Ik%DFr|I%( zyqB8!dByI=J&OlrU6Qo~r;PqO9rY@|CVMaFe&XPnp5c_*`s@#|pxabnK|dakGdurd z(k1ex-ro*xj*GHQ&WTQ#&&R_V`6E4SZ>`>>uSn_S_1)TXHK_eP0A*%NQ?V{U2@VzuzcutgH~KG2?d zlh6xI^7dNndrO})o_WQ8B8uTyj|@f`hpG3xI>Ti@^zq7z7TJ$xDk^1`A4{tz4;Md8 zecK}22;?`HZI_3QZ*bTiCd0bl3u~J0`}*@M-`ZF&F!co17u?ggsmbr#{^zYm%INd{ znuwtLi);reTw7?EII8-o{Ec=@5oynBvncPLUJDVRlau17CA(g8BrDb;{@O{Ar__PF zp?#BFPM93N@B%Eo$G2s43vfQEObP&cGa}|GpQ4*nqapp~;uWVGz8j)qVo!d4K8xeT zz^+%)$7E&?8w@MR6MX=MgxtP8w7FuQ`=(IjnCvOg8%4r5^$umnsy^c$5e$_}b|1OK z-X2ryUMe;;EPQrdU5Q_!cPAl5ZE9WHcfi!ivN~jPKJcXnMYP6_*Rhh0KOM~kG->)g z)! z&7;2aMR2u9?8~|O z;i7QEQu4U_qrAmM?kz5?Zcntba`+C0^?VB*QJEqzkxxDDJJI)5{Yu<@UqS{kNkjU{OXM9nJ%n8H!0 z;e&q1n`LHZ#;F`$6*_?F!_GheWi6<0%>2NK6I&$8HL2-?5sMoG!j_}Heh zN|=mFU!ODLJP)5tSU>lCmSA?*0^?vemo z%IUht)M+fbLfOp$>LaQz-ZONp4PPtd+&5G5W7;1L2;IT5SNd5sZTvD+I&0y2!9TRJ ztdqk=zJ(gbN-{i~(OTH)U~r@QW6qX{SNV5Xx_dSY+UUYBS_szvx$gGp`mk11jvMQ$ z+@YUievR$XD;}v1tcCVFd8TkpchGWey$(+ljZbnqf>q+srSKLX$z8b!QyZX|_hR)7 zoQK{#g9C)~yR%_`L9>y4q%%mql>nPKWY61|ce!Z^SgRxGIC0Mu85LAkdSRtcp2Tix zlb)Uq_S9zHc!H;7Xl%c(^CMLuN=hm!3X1Ap3B6feH_rHkgoKzRjy>;N+d6vR^{dR% zkEf^GsHwBA7Mbo}yB$oo<&1B>e#@0YHt)r3K1nk_ZDNRE^1{>h!m#wS+gHyVYs_c) zG;V&QUN50K%{EJyOOUZ(PJ=f*G^;;jx54?k7!x^x;&I{f%*919scj47c4DpXr_ul% zJ$(2OFZ=hm-cwM9Jv=;qq4fa->HR3@@+PoINHy5>%>bgJr6(sRkGX$8Sb>F!33pJa zI|5@qP30aeey!h9b#E3lXA)0$C~Z>%0TgeYYPzAv3qxwt2IE zT2{@DQn-Y@cr;Tyy6wbfC{<*l;?#_4YsJM6hLPGWU}E0@_~l1KjKc*Eaz~&l10ul1 z&(_WP`7wx=Unem(Ia$$jt`ExvZP9uhB;5IIKj z%V9&^qGBsWUjyC7Z18n`Rn>R6rg&UX=1ErU#WxS#fOz?O`3Dc3XZRC|@IX)Vhx{G~ z&7daR+V*IN*=$5F&ZfD3U3S}`uFd$`cbMWY{jHc~j?eF6mL2G~7@w$qOgq;#W5yxW za)srQbo9Kz>61?UUcFjgUpLA#B-~3$ak_MAW_&z4DT(CJ zMeR{QwcMf%tr|xa+|iK0s0q+xadZdoT~bog%(4wfu1)|40A+_9!5quOBwjf96vQTU zTpZ}HczSNT4zCNq=k@CZE9q7A*1b8Wet!QxXscwLbh6onA$q5@%I+{OtKDJjdC}23 z2~-!|C0kbBTLnBQP8-V&o)J-WDAv*Fy!x$>r}zl(7bboGLu0p`_~;4u5)Cwb8bn0} zl$_{HwSsuEC8D*SUytXLt}60Y=6G4|rlT$zqr*$z>889h%`fYo{+^r9)#P8@Ulobl z9%vKcZS?#K%U*TfZ24#Rz3j3Y75hZ=qDCktSx<655j-LKF5W>ze%Gb_t~++Ezhx50 zjLo>Y;+3|k5hh>BFf>j>qXYgEfk)c2XTJ4WhPzozi+IhS(%!OlcjEmMg127&DZSWY zJiX!AmpOV=Zl+zotpA7fMe1X7ORhWAc^}A2e&R6W7q}PR*m8qrBmMp6A!xDnD>{t- zex}lH_E>#Ng3@9laen=(lN5c2y4;Rqay!TgCk{t7m<}k>k5q|n)ge$1`)^Jl zOK@6(b$PwWc=8L!376eE1-C9tV3wR)kJOoYbsqOKsR- zOgEFP)_2Zv|GK=sLVI8Ei=)LJj1@SNKOT}QaF#EBTbbKFNw&2F)|h-@ZNSwTd@Y!b zRBRa#$QG%YVu=YF{-2i7lKp*mde1)n3_T`|?+Op?BDv@u(VY~U<#~ExU;aU{ z>W8k10s^^Lev(zS4w3VcQbW~A1rE0A-e#K*{wp@*9cZMeHrxF&R9z6$9xhr=n>g68 zmOJ@M&9VFKK%8vi;AtjtUWqh~QL-XO3)XM5Wv2grcC2gRm6Mlbjaki=C`50Xh&XfX znVy^QUtIdV$B+8CVenfgjMfkvQ^vTJu?6jU8AL=HE)8VnJa(xs5As# zmg!JRAJhfCe%D{c`a{liVCBji{-8j;r$zHO^$ad0xveF{--pO7PCFD_Ss4$lpucYy z_(tDBLz*ZHzm3(EwH<_N)uAGnCP9 zskoK0eypb)!SER-rg$^axEk|u_7PSlop>WdVGBj>mC50~3=dd7ft8r_w&wt4Fgkj4 z|F&wC@$QXlGJSV)IXP>--TC?R?I?`}(TcH~@5x@-n(TZAf_=WwF88vMr9z2)40*Sr zvp%tu6|RkT-CB&&+E?M_5v!F+`_qm4PnJ;}OL^E%zwdV==2q5MPIkO{uGP0?jqR|= z`15X+4E{kP@9d3&_vhl0A6}2nm3Yn&R&zV2EMk6-lgHgfF-n13R_YVC>+V!h^G0{4 zmX#Td{`7fUr#}4HkDow6wP(DV~cPf zIowY5vy+Y}{ow;mxt=1POp3xbjr9I38Mh%z z%`dGbmNvJ&=pTAPP581W#nD1KIPoBb(tLmmfx^j;cd#@eGij;eUxbMJheG-RWtG5af0-9+f7Yg ziHzUP`Wk$I>BEpxbIj!D4-XiuoA#GRFPyErfA#!KT!AqvoQEI6m-c#Y)N&pP2J>^jQH*z{u4qeHe6Q?e^P7jkzwCk(YnE`|)=Tq<7TnlV$$%|Te z&2C$9spg(0+0#jT2|t*pT!nIESIdL4HKZt9L)hvb%4&vj-@P!rX|PBWTSMOe?lC*} zi3SPnU+OOWuc+1E1Wp!Gv$#t|whK1&PX$E0KsHP8!@JKO+k;f9EcEr8SAU5vl0-J* zF_My^Ktx;d6k_7Am|T>wsA!RSYHI3vV5D9iov6s>ebq+Tp{+=NU|Ks&B}9aW!*mWf z_CN*s`yO@kusfQQXp`sn+zAig1OeN6hs|K)ytnsv7_c-1z`Rf$EUc}0yH>&fLE%AO zYOPbx%#7$bREq8W5LGXfd!X<&H8tdo&=QaST5bUvlh;4@xi^QL&?0{H=ursBr;e<} zB9Y+g6*Y;xOEub32%Rw0SkL)Oh=>%fPt9~a4xCBxnmaN94f&DxL>TcHpg><&HF#;J&KA=n)^hpfnzRSS08kE!bf0M8)$}+ z6Kr)O!G{pC)TE`7I$$OwO4w*FM3dm+giDVu|0=^lbD+pv_QGqX@pd#hY==n+8_U26#u9DIBK~bm$y`uFf!J zp5Y6iLCgHvf`=kXsvA?+!qB)<5pa@35}_9-S4slxV8yP-hutQEXpdq}A&WMVNc`8e z+1x!MgYX4b>$#&0$^3&jh9uJgoxdAxh9)M5T|YL$cn`q2PNW^iq0GU9&bUV)Kq1OU z%Itt2gvkNCfj6d&@DbD}E;gt$T1raZ;FFJ!T4Z$B!tcj*goCmhKMmf9n(Ch)u4^kR z40Ha7`+)^Rf{aXAUHvz|L&OUPZf=+P163hC`3`@MR?RF0`5CU$L@&sDMt=S{KIh+i zKVkoeuTq_k)x}LgX^lfP$+6g;OiMSC$JL4S1kMNF3U91SQcUNjCNnF(V52yYJsTMJ zM8J_%!&YxOdl>j*KiH0 zZe}Ld|}$Xi=*}XXP0B5quqY2 zOykBt7o9eIKsOnoQP^gu^9(DlE$t2F3edqA))&@1mdQB6lDrBh!c(&%62Ybgr;s>} z=XC@J+;XyRWkl;qjwZh_J%1^vOEOWOhth-E^qqcS3_GQ^t`2DQ*O3}8_%U6H((?#h z{(-pUY?gowlNi7znCpoBF(jVGLIxD{&n+!M1fK%CJsJsSA;H;PF;!p^OCxk=P^2H0Cw)uSZELFu;#^p%Z` z;|2No^4P`;-fm`yl^yCSwmiOz+L^ikb>-q$;RMqJQ&X!n9*L4-&$~I<+1XiH8o19i zHDyT5x>g<2eUzN&XnGbF7A~&Qhm)#DIP(AM+`Tj=&Rk6-`oinNMZJpYeUXtx9hzar za_hG)uuv#3EMKNqqyC)B*;1(jZtLb+Z#nmtNWVOTe znsWESD(u>|3y*dJ7^^;mQwR+x96tOH{J$3;@AYL+g78r4at~~gax>9ZH_(3IIcI^i z*`F#0%t;CcB3KBoot~GZN$$SY*4}QjJNxNVSFqU73={EWp=gr$d=B9<*!d9iW%>E} z1qAG1YN3ANVwu%k6(u}xZG{<1-Ut?Btn3m-)!vVZndwNH`S_8Pb51*RoHv?^OOce6 zK!rxu9qat<=C)l3r;-8+0|Bv!m7U>_f%Q7n)RYEG8HVZ{^a*G%B-d2L6csNfB}MdC zBLWh5+$cpYHx4QtnD=jPQ}BJSc)yiiRn|VPr2v5Y%&#YK8jFk^#CO9lb9Q5m)U*-b zZGUVV1sV&8!Wir*x;XpklaZ-u+p7QN%Xv7w?CFoWFtjli7}#=)S_~E)R5wm`c2cU2 za%>n5_x=4Ru6`LDUs^)V1P#~;6xa>^a3D4&AW$c?wF!;g-EOFAE_G|~um*w1i~js0 z1;BL7qlv+aC#LYt9pBbNWu;dahWz#)Dj5BWO%xtzPFB`P_g5HM_@9!JB&*v9JMNaH+SSgQrFwIHef^>tm(o5u9!+5~f~vC1M0;PMcqs8~$LEf} zQp^b=!3To3Y*B14<8L-4cvEUcjvG1YS`a;sYilExZ3`v|9K#i4^1yE3aRZpwSAV&# zu1;7;h;ZHN&t)f&TKIdutZMao)h zdv4AidEVhI5KJLeLudwe1387+Og=0pJ863QQV5HZ9|yVjmtv>XuZm zuvdxaYG$VIZ;8gvkzCUX24oKg77kDPSn@`r zln+G01JvM+27!6$66O_aX3)!T*@}F3_T?QrZsSaPi>rW09PaFl%ElMWptNG@-v*tm zkdV;C;o^$OOmr;ZwF!--UvSIAM+$j;7EAPGU_fR|iHNZOuK}7hR}YWcwH;j}Ztm{H z&o}ostMWHB70vCTrJ&)Gv30f6`b{Q4Pfw3WpvJAQujuZ81MimW2tvbsez&LO+Wh~% zfauZLwKa;1i*X6^oc6DHN}d%h;I=lQ(YE(iP1WprB3G>2QE6x|N6-?JdfXL#a=~fI zu{G8ZBSV#9`gXcZn?53A6SwBh0rh};?;IG1@o_5`(x1nV9xZ#Cy0 z>9DXcq{xs3D6BI^dizsH(t(o?=@=Oe6ygLB|3MLe5%Br*=jOX@D1QhsoY+Zk<_owF zc?2Ng7><3V_cd7Tv$fvcU0tUHK2A>ZA?fKmFA9hQJ2IrUQE%47?D@q|^Nn^8Krmc6 zNLRA12D@_F;$w-XrY7{mO90v7KVV9NnyM-`Hw^VR6awW12qR$p=+|BLzaOr2j>>fW z{{FH1ZtFVH<@8I8-)+%8B1_ z&aV{})C-KO^SQxD8Zc7j4#ot~GOh6NN4+N_hg}fuGDhkMdz9mBJ?8sbmzY2wRa437 zg|R}xOp5&8VLMchYzy?&HrrA>4(~cuFl`OvwlM{Fad3GD7=#}s^foSv2SsdoKiz4Z zo9CjXrl6sws4p>!j*ea$n zPE^s!>bNf3xva@uLWMOk9{lY}k2j~;Why`P@SWA&c~-PMv8a4pc~<2RQomViMLlJ% z3FW7ZZEZ8@NSY?L3n=)oJc%Ib@iK|gq1l%cPOhVFqMZ7nVl_8G{Y3cR3#9^Q1Hc2c0h`YNsp>dL0_6b8cNGGtluk`m6^s_75-vWfA7KfuH$9Ye8@D z7QsVbc=7h_kNt(w4(oi2GBB8sgw^K4<0z*}e}DDbNxhzSPn$xQZJN}+4Sp6rL4`6T z-FX{6zan_``_G?C2!%S|K)H$Hlq+4=auPz;@xJRM;2nB)ol3_cUe2LPm&Y%x$p}S8 z@6k0{U9>AA2F-0n(E|4H#C!p#rlaVT;qTApN1$+oh50;n*%hqU{;rQN+EA!Sfe@N5 z5EvLZUY1sjXYMbkc5cl0z~(0xH4pz0HD|u)L?5(GYCOY&Pg=Na-G@-3#Riv{+~YHm<5PREwY8>D1x{l@?2TviiCi(X@&ye2vRRM-cFq&jmk^tG$W5=$D3mfUR) zlp@WEpdTrK2x?)>qFgXMU-md7PcRo}_&rVMMs<6k*Bk~>880+TT0|*12cNm4C8(%( z-HHMi;P{J?I>7i1Th@SAG6fEPUROi>uG+MkPJSlMXmEIvP8Y^*79UFYN^tUBviM`I z%EmQ?hsQPEiW+{9e%L7_WBEp!OxfdlVR`(N2XWjsvB|b*gIvQ`UEtB|{O&LNv&=^0 zxV8t~nDI$#s0ypPXYplj(TX(mYw_02Pg5Q?UOjPI`^sZZv*+biGVs_kGHtWe_2_av zy(wLx>Nj%O;n1#}Vw1w;T!U2g{W)aHf?0obyAPa|NGw`AW?M5H;d@Ldz+%6sIIRlb za5_dteZ~hwz3ph*Zy0e5KzqZ8o=jVOMM7&gvhDYV`*vETgq) zZ!0)dV-l7vV~2a5W-u2Gz1p_w-hhnf`nNt*x$|vPj@b81OWU9?a_m9)-hw2+QZTamg^Qx3GN)BtxQuHCciy}QH zZ#jve;p1nhy9RSY4Ap;c^k&{S2$CQESruyjDc2t3r=pfmnFKqx=xTb}i?$uK68nVh zT8FhLm0+uQ;%oA@>!ED_~E z_oW$?4t%heaX7x``SwGdc8UDKdf$`UvUl=Gjii%HbciQl!t+45TvWNI#oHF;(@ZA> zcF4Mz8*k<5Qy<#P5SArbHY5_$<`}RbnmFXhaw4muI7GB9QxZf(MiW8tc87-O79HVz z`XQ>9=Y97ECkg=as&3XTkUhVzR5Zm>%y@8!$WpP7Az}|_%9lNy7(VxHS&u1Xp%=s4 zpiozPd$Y#J5~~lt-yAl7HgR8{m6qFlp!ubl_W%!cU!vo3^v7`y*CO%9lZQ!T%3ipg zwX(W#+vPfBP0|PfX3%)re#Z_Nciky0sHk6$o7Xm~h$+bF8SMsXLW+!0zW3k^R`+_{ zQEg^qd11cYCG7QUVdNE5(-lY)>mcVcS|@4kGZ1+Ha$Q$j-rVR$-fWssK`z7vM~?UA zw^p6;d(z|`WorA{Iu6NKcv(*j_1zJPnsoZDsJLkob)w_d$&-q(!QcPK1}FW_xR+ud zPyF{2uExhbYTI|o@??Ioz}nYc%PStnubC-*A}mE_>JL49vpuM!D)ZUXym_(t=qP8T|_p|TIFwHBP$I!Z29>2Fu6Mv5J5>l_*cstrC(}ssOUnG5M-3D95G-=l1|LEu7 zc`<|uBw9IInA`eEttzj-P{V6@pLrC>Af3C{fow-B + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [billing_account](variables.tf#L16) | Billing account id used as default for new projects. | string | ✓ | | +| [project_id](variables.tf#L43) | Existing project id. | string | ✓ | | +| [tfe_organization_id](variables.tf#L48) | TFE organization id. | string | ✓ | | +| [tfe_workspace_id](variables.tf#L53) | TFE workspace id. | string | ✓ | | +| [issuer_uri](variables.tf#L21) | Terraform Enterprise uri. Replace the uri if a self hosted instance is used. | string | | "https://app.terraform.io/" | +| [parent](variables.tf#L27) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [project_create](variables.tf#L37) | Create project instead of using an existing one. | bool | | true | +| [workload_identity_pool_id](variables.tf#L58) | Workload identity pool id. | string | | "tfe-pool" | +| [workload_identity_pool_provider_id](variables.tf#L64) | Workload identity pool provider id. | string | | "tfe-provider" | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [impersonate_service_account_email](outputs.tf#L16) | Service account to be impersonated by workload identity. | | +| [project_id](outputs.tf#L21) | GCP Project ID. | | +| [workload_identity_audience](outputs.tf#L26) | TFC Workload Identity Audience. | | +| [workload_identity_pool_provider_id](outputs.tf#L31) | GCP workload identity pool provider ID. | | + + diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/README.md b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/README.md new file mode 100644 index 0000000..2c6faee --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/README.md @@ -0,0 +1,75 @@ +# Google Service Account Module + +This module allows simplified creation and management of one a service account and its IAM bindings. A key can optionally be generated and will be stored in Terraform state. To use it create a sensitive output in your root modules referencing the `key` output, then extract the private key from the JSON formatted outputs. Alternatively, the `key` can be generated with `openssl` library and only public part uploaded to the Service Account, for more refer to the [Onprem SA Key Management](../../blueprints/cloud-operations/onprem-sa-key-management/) example. + +Note that this module does not fully comply with our design principles, as outputs have no dependencies on IAM bindings to prevent resource cycles. + +## Example + +```hcl +module "myproject-default-service-accounts" { + source = "./fabric/modules/iam-service-account" + project_id = "myproject" + name = "vm-default" + generate_key = true + # authoritative roles granted *on* the service accounts to other identities + iam = { + "roles/iam.serviceAccountUser" = ["user:foo@example.com"] + } + # non-authoritative roles granted *to* the service accounts on other resources + iam_project_roles = { + "myproject" = [ + "roles/logging.logWriter", + "roles/monitoring.metricWriter", + ] + } +} +# tftest modules=1 resources=5 +``` + + + +## Files + +| name | description | resources | +|---|---|---| +| [iam.tf](./iam.tf) | IAM bindings. | google_billing_account_iam_member · google_folder_iam_member · google_organization_iam_member · google_project_iam_member · google_service_account_iam_binding · google_service_account_iam_member · google_storage_bucket_iam_member | +| [main.tf](./main.tf) | Module-level locals and resources. | google_service_account · google_service_account_key | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [variables.tf](./variables.tf) | Module variables. | | +| [versions.tf](./versions.tf) | Version pins. | | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [name](variables.tf#L91) | Name of the service account to create. | string | ✓ | | +| [project_id](variables.tf#L106) | Project id where service account will be created. | string | ✓ | | +| [description](variables.tf#L17) | Optional description. | string | | null | +| [display_name](variables.tf#L23) | Display name of the service account to create. | string | | "Terraform-managed." | +| [generate_key](variables.tf#L29) | Generate a key for service account. | bool | | false | +| [iam](variables.tf#L35) | IAM bindings on the service account in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive](variables.tf#L42) | IAM additive bindings on the service account in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_billing_roles](variables.tf#L49) | Billing account roles granted to this service account, by billing account id. Non-authoritative. | map(list(string)) | | {} | +| [iam_folder_roles](variables.tf#L56) | Folder roles granted to this service account, by folder id. Non-authoritative. | map(list(string)) | | {} | +| [iam_organization_roles](variables.tf#L63) | Organization roles granted to this service account, by organization id. Non-authoritative. | map(list(string)) | | {} | +| [iam_project_roles](variables.tf#L70) | Project roles granted to this service account, by project id. | map(list(string)) | | {} | +| [iam_sa_roles](variables.tf#L77) | Service account roles granted to this service account, by service account name. | map(list(string)) | | {} | +| [iam_storage_roles](variables.tf#L84) | Storage roles granted to this service account, by bucket name. | map(list(string)) | | {} | +| [prefix](variables.tf#L96) | Prefix applied to service account names. | string | | null | +| [public_keys_directory](variables.tf#L111) | Path to public keys data files to upload to the service account (should have `.pem` extension). | string | | "" | +| [service_account_create](variables.tf#L117) | Create service account. When set to false, uses a data source to reference an existing service account. | bool | | true | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [email](outputs.tf#L17) | Service account email. | | +| [iam_email](outputs.tf#L25) | IAM-format service account email. | | +| [id](outputs.tf#L33) | Service account id. | | +| [key](outputs.tf#L42) | Service account key. | ✓ | +| [name](outputs.tf#L48) | Service account name. | | +| [service_account](outputs.tf#L57) | Service account resource. | | +| [service_account_credentials](outputs.tf#L62) | Service account json credential templates for uploaded public keys data. | | + + diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/iam.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/iam.tf new file mode 100644 index 0000000..02c879d --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/iam.tf @@ -0,0 +1,145 @@ +/** + * 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 + * + * 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 IAM bindings. + +locals { + _iam_additive_pairs = flatten([ + for role, members in var.iam_additive : [ + for member in members : { role = role, member = member } + ] + ]) + iam_additive = { + for pair in local._iam_additive_pairs : + "${pair.role}-${pair.member}" => pair + } + iam_billing_pairs = flatten([ + for entity, roles in var.iam_billing_roles : [ + for role in roles : [ + { entity = entity, role = role } + ] + ] + ]) + iam_folder_pairs = flatten([ + for entity, roles in var.iam_folder_roles : [ + for role in roles : [ + { entity = entity, role = role } + ] + ] + ]) + iam_organization_pairs = flatten([ + for entity, roles in var.iam_organization_roles : [ + for role in roles : [ + { entity = entity, role = role } + ] + ] + ]) + iam_project_pairs = flatten([ + for entity, roles in var.iam_project_roles : [ + for role in roles : [ + { entity = entity, role = role } + ] + ] + ]) + iam_sa_pairs = flatten([ + for entity, roles in var.iam_sa_roles : [ + for role in roles : [ + { entity = entity, role = role } + ] + ] + ]) + iam_storage_pairs = flatten([ + for entity, roles in var.iam_storage_roles : [ + for role in roles : [ + { entity = entity, role = role } + ] + ] + ]) +} + +resource "google_service_account_iam_member" "roles" { + for_each = local.iam_additive + service_account_id = local.service_account.name + role = each.value.role + member = each.value.member +} + +resource "google_service_account_iam_binding" "roles" { + for_each = var.iam + service_account_id = local.service_account.name + role = each.key + members = each.value +} + +resource "google_billing_account_iam_member" "billing-roles" { + for_each = { + for pair in local.iam_billing_pairs : + "${pair.entity}-${pair.role}" => pair + } + billing_account_id = each.value.entity + role = each.value.role + member = local.resource_iam_email +} + +resource "google_folder_iam_member" "folder-roles" { + for_each = { + for pair in local.iam_folder_pairs : + "${pair.entity}-${pair.role}" => pair + } + folder = each.value.entity + role = each.value.role + member = local.resource_iam_email +} + +resource "google_organization_iam_member" "organization-roles" { + for_each = { + for pair in local.iam_organization_pairs : + "${pair.entity}-${pair.role}" => pair + } + org_id = each.value.entity + role = each.value.role + member = local.resource_iam_email +} + +resource "google_project_iam_member" "project-roles" { + for_each = { + for pair in local.iam_project_pairs : + "${pair.entity}-${pair.role}" => pair + } + project = each.value.entity + role = each.value.role + member = local.resource_iam_email +} + +resource "google_service_account_iam_member" "additive" { + for_each = { + for pair in local.iam_sa_pairs : + "${pair.entity}-${pair.role}" => pair + } + service_account_id = each.value.entity + role = each.value.role + member = local.resource_iam_email +} + +resource "google_storage_bucket_iam_member" "bucket-roles" { + for_each = { + for pair in local.iam_storage_pairs : + "${pair.entity}-${pair.role}" => pair + } + bucket = each.value.entity + role = each.value.role + member = local.resource_iam_email +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/main.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/main.tf new file mode 100644 index 0000000..2c9ee36 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/main.tf @@ -0,0 +1,87 @@ +/** + * 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 + * + * 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 { + # https://github.com/hashicorp/terraform/issues/22405#issuecomment-591917758 + key = try( + var.generate_key + ? google_service_account_key.key["1"] + : map("", null) + , {}) + prefix = var.prefix == null ? "" : "${var.prefix}-" + resource_email_static = "${local.prefix}${var.name}@${var.project_id}.iam.gserviceaccount.com" + resource_iam_email = ( + local.service_account != null + ? "serviceAccount:${local.service_account.email}" + : local.resource_iam_email_static + ) + resource_iam_email_static = "serviceAccount:${local.resource_email_static}" + service_account_id_static = "projects/${var.project_id}/serviceAccounts/${local.resource_email_static}" + service_account = ( + var.service_account_create + ? try(google_service_account.service_account.0, null) + : try(data.google_service_account.service_account.0, null) + ) + service_account_credential_templates = { + for file, _ in local.public_keys_data : file => jsonencode( + { + type : "service_account", + project_id : var.project_id, + private_key_id : split("/", google_service_account_key.upload_key[file].id)[5] + private_key : "REPLACE_ME_WITH_PRIVATE_KEY_DATA" + client_email : local.resource_email_static + client_id : local.service_account.unique_id, + auth_uri : "https://accounts.google.com/o/oauth2/auth", + token_uri : "https://oauth2.googleapis.com/token", + auth_provider_x509_cert_url : "https://www.googleapis.com/oauth2/v1/certs", + client_x509_cert_url : "https://www.googleapis.com/robot/v1/metadata/x509/${urlencode(local.resource_email_static)}" + } + ) + } + public_keys_data = ( + var.public_keys_directory != "" + ? { + for file in fileset("${path.root}/${var.public_keys_directory}", "*.pem") + : file => filebase64("${path.root}/${var.public_keys_directory}/${file}") } + : {} + ) +} + + +data "google_service_account" "service_account" { + count = var.service_account_create ? 0 : 1 + project = var.project_id + account_id = "${local.prefix}${var.name}" +} + +resource "google_service_account" "service_account" { + count = var.service_account_create ? 1 : 0 + project = var.project_id + account_id = "${local.prefix}${var.name}" + display_name = var.display_name + description = var.description +} + +resource "google_service_account_key" "key" { + for_each = var.generate_key ? { 1 = 1 } : {} + service_account_id = local.service_account.email +} + +resource "google_service_account_key" "upload_key" { + for_each = local.public_keys_data + service_account_id = local.service_account.email + public_key_data = each.value +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/outputs.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/outputs.tf new file mode 100644 index 0000000..e6c28df --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/outputs.tf @@ -0,0 +1,65 @@ +/** + * 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 + * + * 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. + */ + +output "email" { + description = "Service account email." + value = local.resource_email_static + depends_on = [ + local.service_account + ] +} + +output "iam_email" { + description = "IAM-format service account email." + value = local.resource_iam_email_static + depends_on = [ + local.service_account + ] +} + +output "id" { + description = "Service account id." + value = local.service_account_id_static + depends_on = [ + data.google_service_account.service_account, + google_service_account.service_account + ] +} + +output "key" { + description = "Service account key." + sensitive = true + value = local.key +} + +output "name" { + description = "Service account name." + value = local.service_account_id_static + depends_on = [ + data.google_service_account.service_account, + google_service_account.service_account + ] +} + +output "service_account" { + description = "Service account resource." + value = local.service_account +} + +output "service_account_credentials" { + description = "Service account json credential templates for uploaded public keys data." + value = local.service_account_credential_templates +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/variables.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/variables.tf new file mode 100644 index 0000000..a9f60bf --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/variables.tf @@ -0,0 +1,121 @@ +/** + * 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 + * + * 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. + */ + +variable "description" { + description = "Optional description." + type = string + default = null +} + +variable "display_name" { + description = "Display name of the service account to create." + type = string + default = "Terraform-managed." +} + +variable "generate_key" { + description = "Generate a key for service account." + type = bool + default = false +} + +variable "iam" { + description = "IAM bindings on the service account in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_additive" { + description = "IAM additive bindings on the service account in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_billing_roles" { + description = "Billing account roles granted to this service account, by billing account id. Non-authoritative." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_folder_roles" { + description = "Folder roles granted to this service account, by folder id. Non-authoritative." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_organization_roles" { + description = "Organization roles granted to this service account, by organization id. Non-authoritative." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_project_roles" { + description = "Project roles granted to this service account, by project id." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_sa_roles" { + description = "Service account roles granted to this service account, by service account name." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_storage_roles" { + description = "Storage roles granted to this service account, by bucket name." + type = map(list(string)) + default = {} + nullable = false +} + +variable "name" { + description = "Name of the service account to create." + type = string +} + +variable "prefix" { + description = "Prefix applied to service account names." + type = string + default = null + validation { + condition = var.prefix != "" + error_message = "Prefix cannot be empty, please use null instead." + } +} + +variable "project_id" { + description = "Project id where service account will be created." + type = string +} + +variable "public_keys_directory" { + description = "Path to public keys data files to upload to the service account (should have `.pem` extension)." + type = string + default = "" +} + +variable "service_account_create" { + description = "Create service account. When set to false, uses a data source to reference an existing service account." + type = bool + default = true +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/versions.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/versions.tf new file mode 100644 index 0000000..286536a --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/iam-service-account/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.3.1" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.40.0" # tftest + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.40.0" # tftest + } + } +} + + diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/main.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/main.tf new file mode 100644 index 0000000..200f9fd --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/main.tf @@ -0,0 +1,99 @@ +# 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. + + +############################################################################### +# GCP PROJECT # +############################################################################### + +# module "project" { +# #count = var.target_group_addition ? 1 : 0 +# source = "./project" +# name = var.project_id +# project_create = var.project_create +# parent = var.parent +# billing_account = var.billing_account +# services = [ +# "iam.googleapis.com", +# "cloudresourcemanager.googleapis.com", +# "iamcredentials.googleapis.com", +# "sts.googleapis.com", +# "storage.googleapis.com" +# ] +# } + +############################################################################### +# Workload Identity Pool and Provider # +############################################################################### + +resource "google_iam_workload_identity_pool" "tfe-pool-ff" { + #project = module.project.project_id + project = var.project_id + workload_identity_pool_id = var.workload_identity_pool_id + display_name = "TFE Pool ff" + description = "Identity pool for Terraform Enterprise OIDC integration" +} + +resource "google_iam_workload_identity_pool_provider" "tfe-pool-provider" { + #project = module.project.project_id + project = var.project_id + workload_identity_pool_id = google_iam_workload_identity_pool.tfe-pool-ff.workload_identity_pool_id + workload_identity_pool_provider_id = var.workload_identity_pool_provider_id + display_name = "TFE Pool Provider ff" + description = "OIDC identity pool provider for TFE Integration ff" + # Use condition to make sure only token generated for a specific TFE Org can be used across org workspaces + attribute_condition = "attribute.terraform_organization_id == \"${var.tfe_organization_id}\"" + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.aud" = "assertion.aud" + "attribute.terraform_run_phase" = "assertion.terraform_run_phase" + "attribute.terraform_workspace_id" = "assertion.terraform_workspace_id" + "attribute.terraform_workspace_name" = "assertion.terraform_workspace_name" + "attribute.terraform_organization_id" = "assertion.terraform_organization_id" + "attribute.terraform_organization_name" = "assertion.terraform_organization_name" + "attribute.terraform_run_id" = "assertion.terraform_run_id" + "attribute.terraform_full_workspace" = "assertion.terraform_full_workspace" + } + oidc { + # Should be different if self hosted TFE instance is used + issuer_uri = var.issuer_uri + } +} + +############################################################################### +# Service Account and IAM bindings # +############################################################################### + +module "sa-tfe" { + source = "./iam-service-account" + #project_id = module.project.project_id + project_id = var.project_id + name = "sa-tfe-ff" + + iam = { + # We allow only tokens generated by a specific TFE workspace impersonation of the service account, + # that way one identity pool can be used for a TFE Organization, but every workspace will be able to impersonate only a specifc SA + "roles/iam.workloadIdentityUser" = ["principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.tfe-pool-ff.name}/attribute.terraform_workspace_id/${var.tfe_workspace_id}"] + #"roles/iam.workloadIdentityUser" = ["principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.tfe-pool-ff.name}"] + } + # module.project.project_id + # var.project_id + iam_project_roles = { + "${var.project_id}" = [ + "roles/storage.admin", + "roles/compute.networkAdmin", + "roles/compute.networkUser" + ] + } +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/outputs.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/outputs.tf new file mode 100644 index 0000000..a7574af --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/outputs.tf @@ -0,0 +1,35 @@ +# 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. + + +output "impersonate_service_account_email" { + description = "Service account to be impersonated by workload identity." + value = module.sa-tfe.email +} + +output "project_id" { + description = "GCP Project ID." + #value = module.project.project_id + value = var.project_id +} + +output "workload_identity_audience" { + description = "TFC Workload Identity Audience." + value = "//iam.googleapis.com/${google_iam_workload_identity_pool_provider.tfe-pool-provider.name}" +} + +output "workload_identity_pool_provider_id" { + description = "GCP workload identity pool provider ID." + value = google_iam_workload_identity_pool_provider.tfe-pool-provider.name +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/README.md b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/README.md new file mode 100644 index 0000000..b6fb388 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/README.md @@ -0,0 +1,522 @@ +# Project Module + +This module implements the creation and management of one GCP project including IAM, organization policies, Shared VPC host or service attachment, service API activation, and tag attachment. It also offers a convenient way to refer to managed service identities (aka robot service accounts) for APIs. + +## IAM Examples + +IAM is managed via several variables that implement different 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` and `iam_additive_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 + +Be mindful about service identity roles when using authoritative IAM, as you might inadvertently remove a role from a [service identity](https://cloud.google.com/iam/docs/service-accounts#google-managed) or default service account. For example, using `roles/editor` with `iam` or `group_iam` will remove the default permissions for the Cloud Services identity. A simple workaround for these scenarios is described below. + +### Authoritative IAM + +The `iam` variable is based on role keys and is typically used for service accounts, or where member values can be dynamic and would create potential problems in the underlying `for_each` cycle. + +```hcl +locals { + gke_service_account = "my_gke_service_account" +} + +module "project" { + source = "./fabric/modules/project" + billing_account = "123456-123456-123456" + name = "project-example" + parent = "folders/1234567890" + prefix = "foo" + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + iam = { + "roles/container.hostServiceAgentUser" = [ + "serviceAccount:${local.gke_service_account}" + ] + } +} +# tftest modules=1 resources=4 +``` + +The `group_iam` variable uses group email addresses as keys and is a convenient way to assign roles to humans following Google's best practices. The end result is readable code that also serves as documentation. + +```hcl +module "project" { + source = "./fabric/modules/project" + billing_account = "123456-123456-123456" + name = "project-example" + parent = "folders/1234567890" + prefix = "foo" + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + group_iam = { + "gcp-security-admins@example.com" = [ + "roles/cloudasset.owner", + "roles/cloudsupport.techSupportEditor", + "roles/iam.securityReviewer", + "roles/logging.admin", + ] + } +} +# tftest modules=1 resources=7 +``` + +### Additive IAM + +Additive IAM is typically used where bindings for specific roles are controlled by different modules or in different Terraform stages. One example is when the project is created by one team but a different team manages service account creation for the project, and some of the project-level roles overlap in the two configurations. + +```hcl +module "project" { + source = "./fabric/modules/project" + name = "project-example" + iam_additive = { + "roles/viewer" = [ + "group:one@example.org", + "group:two@xample.org" + ], + "roles/storage.objectAdmin" = [ + "group:two@example.org" + ], + "roles/owner" = [ + "group:three@example.org" + ], + } +} +# tftest modules=1 resources=5 +``` + +### Service Identities and authoritative IAM + +As mentioned above, there are cases where authoritative management of specific IAM roles results in removal of default bindings from service identities. One example is outlined below, with a simple workaround leveraging the `service_accounts` output to identify the service identity. A full list of service identities and their roles can be found [here](https://cloud.google.com/iam/docs/service-agents). + +```hcl +module "project" { + source = "./fabric/modules/project" + name = "project-example" + group_iam = { + "foo@example.com" = [ + "roles/editor" + ] + } + iam = { + "roles/editor" = [ + "serviceAccount:${module.project.service_accounts.cloud_services}" + ] + } +} +# tftest modules=1 resources=2 +``` + +## Shared VPC service + +The module allows managing Shared VPC status for both hosts and service projects, and includes a simple way of assigning Shared VPC roles to service identities. + +### Host project + +You can enable Shared VPC Host at the project level and manage project service association independently. + +```hcl +module "project" { + source = "./fabric/modules/project" + name = "project-example" + shared_vpc_host_config = { + enabled = true + } +} +# tftest modules=1 resources=2 +``` + +### Service project + +```hcl +module "project" { + source = "./fabric/modules/project" + name = "project-example" + shared_vpc_service_config = { + attach = true + host_project = "my-host-project" + service_identity_iam = { + "roles/compute.networkUser" = [ + "cloudservices", "container-engine" + ] + "roles/vpcaccess.user" = [ + "cloudrun" + ] + "roles/container.hostServiceAgentUser" = [ + "container-engine" + ] + } + } +} +# tftest modules=1 resources=6 +``` + +## Organization policies + +To manage organization policies, the `orgpolicy.googleapis.com` service should be enabled in the quota project. + +```hcl +module "project" { + source = "./fabric/modules/project" + billing_account = "123456-123456-123456" + name = "project-example" + parent = "folders/1234567890" + prefix = "foo" + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + org_policies = { + "compute.disableGuestAttributesAccess" = { + enforce = true + } + "constraints/compute.skipDefaultNetworkCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(\"tagKeys/1234\", \"tagValues/1234\")" + title = "condition" + description = "test condition" + location = "somewhere" + } + enforce = true + } + ] + } + "constraints/iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } + "constraints/compute.trustedImageProjects" = { + allow = { + values = ["projects/my-project"] + } + } + "constraints/compute.vmExternalIpAccess" = { + deny = { all = true } + } + } +} +# tftest modules=1 resources=10 +``` + +### Organization policy factory + +Organization policies can be loaded from a directory containing YAML files where each file defines one or more constraints. The structure of the YAML files is exactly the same as the `org_policies` variable. + +Note that contraints defined via `org_policies` take precedence over those in `org_policies_data_path`. In other words, if you specify the same contraint in a YAML file *and* in the `org_policies` variable, the latter will take priority. + +The example below deploys a few organization policies split between two YAML files. + +```hcl +module "folder" { + source = "./fabric/modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + org_policies_data_path = "configs/org-policies/" +} +# tftest modules=1 resources=6 files=boolean,list +``` + +```yaml +# tftest file boolean configs/org-policies/boolean.yaml +iam.disableServiceAccountKeyCreation: + enforce: true + +iam.disableServiceAccountKeyUpload: + enforce: false + rules: + - condition: + expression: resource.matchTagId("tagKeys/1234", "tagValues/1234") + title: condition + description: test condition + location: xxx + enforce: true +``` + +```yaml +# tftest file list configs/org-policies/list.yaml +compute.vmExternalIpAccess: + deny: + all: true + +iam.allowedPolicyMemberDomains: + allow: + values: + - C0xxxxxxx + - C0yyyyyyy + +compute.restrictLoadBalancerCreationForTypes: + deny: + values: ["in:EXTERNAL"] + rules: + - condition: + expression: resource.matchTagId("tagKeys/1234", "tagValues/1234") + title: condition + description: test condition + allow: + values: ["in:EXTERNAL"] + - condition: + expression: resource.matchTagId("tagKeys/12345", "tagValues/12345") + title: condition2 + description: test condition2 + allow: + all: true +``` + + +## Logging Sinks (in same project) + +```hcl +module "gcs" { + source = "./fabric/modules/gcs" + project_id = var.project_id + name = "gcs_sink" + force_destroy = true +} + +module "dataset" { + source = "./fabric/modules/bigquery-dataset" + project_id = var.project_id + id = "bq_sink" +} + +module "pubsub" { + source = "./fabric/modules/pubsub" + project_id = var.project_id + name = "pubsub_sink" +} + +module "bucket" { + source = "./fabric/modules/logging-bucket" + parent_type = "project" + parent = "my-project" + id = "bucket" +} + +module "project-host" { + source = "./fabric/modules/project" + name = "my-project" + billing_account = "123456-123456-123456" + parent = "folders/1234567890" + logging_sinks = { + warnings = { + destination = module.gcs.id + filter = "severity=WARNING" + type = "storage" + } + info = { + destination = module.dataset.id + filter = "severity=INFO" + type = "bigquery" + } + notice = { + destination = module.pubsub.id + filter = "severity=NOTICE" + type = "pubsub" + } + debug = { + destination = module.bucket.id + filter = "severity=DEBUG" + exclusions = { + no-compute = "logName:compute" + } + type = "logging" + } + } + logging_exclusions = { + no-gce-instances = "resource.type=gce_instance" + } +} +# tftest modules=5 resources=14 +``` + +## Logging Sinks (in different project) + +When writing to destinations in a different project, set `unique_writer` to `true`. + +```hcl +module "gcs" { + source = "./fabric/modules/gcs" + project_id = "project-1" + name = "gcs_sink" + force_destroy = true +} + +module "project-host" { + source = "./fabric/modules/project" + name = "project-2" + billing_account = "123456-123456-123456" + parent = "folders/1234567890" + logging_sinks = { + warnings = { + destination = module.gcs.id + filter = "severity=WARNING" + unique_writer = true + type = "storage" + } + } +} +# tftest modules=2 resources=4 +``` + + +## Cloud KMS encryption keys + +The module offers a simple, centralized way to assign `roles/cloudkms.cryptoKeyEncrypterDecrypter` to service identities. + +```hcl +module "project" { + source = "./fabric/modules/project" + name = "my-project" + billing_account = "123456-123456-123456" + prefix = "foo" + services = [ + "compute.googleapis.com", + "storage.googleapis.com" + ] + service_encryption_key_ids = { + compute = [ + "projects/kms-central-prj/locations/europe-west3/keyRings/my-keyring/cryptoKeys/europe3-gce", + "projects/kms-central-prj/locations/europe-west4/keyRings/my-keyring/cryptoKeys/europe4-gce" + ] + storage = [ + "projects/kms-central-prj/locations/europe/keyRings/my-keyring/cryptoKeys/europe-gcs" + ] + } +} +# tftest modules=1 resources=7 +``` + +## 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. + +```hcl +module "org" { + source = "./fabric/modules/organization" + organization_id = var.organization_id + tags = { + environment = { + description = "Environment specification." + iam = null + values = { + dev = null + prod = null + } + } + } +} + +module "project" { + source = "./fabric/modules/project" + name = "test-project" + tag_bindings = { + env-prod = module.org.tag_values["environment/prod"].id + foo = "tagValues/12345678" + } +} +# tftest modules=2 resources=6 +``` + +## Outputs + +Most of this module's outputs depend on its resources, to allow Terraform to compute all dependencies required for the project to be correctly configured. This allows you to reference outputs like `project_id` in other modules or resources without having to worry about setting `depends_on` blocks manually. + +One non-obvious output is `service_accounts`, which offers a simple way to discover service identities and default service accounts, and guarantees that service identities that require an API call to trigger creation (like GCS or BigQuery) exist before use. + +```hcl +module "project" { + source = "./fabric/modules/project" + name = "project-example" + services = [ + "compute.googleapis.com" + ] +} + +output "compute_robot" { + value = module.project.service_accounts.robots.compute +} +# tftest modules=1 resources=2 +``` + + + + +## Files + +| name | description | resources | +|---|---|---| +| [iam.tf](./iam.tf) | Generic and OSLogin-specific IAM bindings and roles. | google_project_iam_binding · google_project_iam_custom_role · google_project_iam_member | +| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_project_exclusion · google_logging_project_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | +| [main.tf](./main.tf) | Module-level locals and resources. | google_compute_project_metadata_item · google_essential_contacts_contact · google_monitoring_monitored_project · google_project · google_project_service · google_resource_manager_lien | +| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_org_policy_policy | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [service-accounts.tf](./service-accounts.tf) | Service identities and supporting resources. | google_kms_crypto_key_iam_member · google_project_default_service_accounts · google_project_iam_member · google_project_service_identity | +| [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project · google_compute_shared_vpc_service_project · google_project_iam_member | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding | +| [variables.tf](./variables.tf) | Module variables. | | +| [versions.tf](./versions.tf) | Version pins. | | +| [vpc-sc.tf](./vpc-sc.tf) | VPC-SC project-level perimeter configuration. | google_access_context_manager_service_perimeter_resource | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [name](variables.tf#L140) | Project name and id suffix. | string | ✓ | | +| [auto_create_network](variables.tf#L17) | Whether to create the default network for the project. | bool | | false | +| [billing_account](variables.tf#L23) | Billing account id. | string | | null | +| [contacts](variables.tf#L29) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | +| [custom_roles](variables.tf#L36) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| [default_service_account](variables.tf#L43) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" | +| [descriptive_name](variables.tf#L49) | Name of the project name. Used for project name instead of `name` variable. | string | | null | +| [group_iam](variables.tf#L55) | 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. | map(list(string)) | | {} | +| [iam](variables.tf#L62) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive](variables.tf#L69) | IAM additive bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive_members](variables.tf#L76) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | map(list(string)) | | {} | +| [labels](variables.tf#L82) | Resource labels. | map(string) | | {} | +| [lien_reason](variables.tf#L89) | If non-empty, creates a project lien with this description. | string | | "" | +| [logging_exclusions](variables.tf#L95) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L102) | Logging sinks to create for this project. | map(object({…})) | | {} | +| [metric_scopes](variables.tf#L133) | List of projects that will act as metric scopes for this project. | list(string) | | [] | +| [org_policies](variables.tf#L145) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | +| [org_policies_data_path](variables.tf#L185) | Path containing org policies in YAML format. | string | | null | +| [oslogin](variables.tf#L191) | Enable OS Login. | bool | | false | +| [oslogin_admins](variables.tf#L197) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | +| [oslogin_users](variables.tf#L205) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | +| [parent](variables.tf#L212) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [prefix](variables.tf#L222) | Optional prefix used to generate project id and name. | string | | null | +| [project_create](variables.tf#L232) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | +| [service_config](variables.tf#L238) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L250) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | +| [service_perimeter_bridges](variables.tf#L257) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | +| [service_perimeter_standard](variables.tf#L264) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | +| [services](variables.tf#L270) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L276) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L285) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | +| [skip_delete](variables.tf#L295) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [tag_bindings](variables.tf#L301) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [custom_roles](outputs.tf#L17) | Ids of the created custom roles. | | +| [name](outputs.tf#L25) | Project name. | | +| [number](outputs.tf#L37) | Project number. | | +| [project_id](outputs.tf#L54) | Project id. | | +| [service_accounts](outputs.tf#L73) | Product robot service accounts in project. | | +| [sink_writer_identities](outputs.tf#L89) | Writer identities created for each sink. | | + + diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/iam.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/iam.tf new file mode 100644 index 0000000..69925cc --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/iam.tf @@ -0,0 +1,115 @@ +/** + * 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 + * + * 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 Generic and OSLogin-specific IAM bindings and roles. + +# IAM notes: +# - external users need to have accepted the invitation email to join +# - oslogin roles also require role to list instances +# - additive (non-authoritative) roles might fail due to dynamic values + +locals { + _group_iam_roles = distinct(flatten(values(var.group_iam))) + _group_iam = { + for r in local._group_iam_roles : r => [ + 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( + try(var.iam[role], []), + 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_project_iam_custom_role" "roles" { + for_each = var.custom_roles + project = local.project.project_id + role_id = each.key + title = "Custom role ${each.key}" + description = "Terraform-managed." + permissions = each.value +} + +resource "google_project_iam_binding" "authoritative" { + for_each = local.iam + project = local.project.project_id + role = each.key + members = each.value + depends_on = [ + google_project_service.project_services, + google_project_iam_custom_role.roles + ] +} + +resource "google_project_iam_member" "additive" { + for_each = ( + length(var.iam_additive) + length(var.iam_additive_members) > 0 + ? local.iam_additive + : {} + ) + project = local.project.project_id + role = each.value.role + member = each.value.member + depends_on = [ + google_project_service.project_services, + google_project_iam_custom_role.roles + ] +} + +resource "google_project_iam_member" "oslogin_iam_serviceaccountuser" { + for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([]) + project = local.project.project_id + role = "roles/iam.serviceAccountUser" + member = each.value +} + +resource "google_project_iam_member" "oslogin_compute_viewer" { + for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([]) + project = local.project.project_id + role = "roles/compute.viewer" + member = each.value +} + +resource "google_project_iam_member" "oslogin_admins" { + for_each = var.oslogin ? toset(var.oslogin_admins) : toset([]) + project = local.project.project_id + role = "roles/compute.osAdminLogin" + member = each.value +} + +resource "google_project_iam_member" "oslogin_users" { + for_each = var.oslogin ? toset(var.oslogin_users) : toset([]) + project = local.project.project_id + role = "roles/compute.osLogin" + member = each.value +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/logging.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/logging.tf new file mode 100644 index 0000000..1db60dc --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/logging.tf @@ -0,0 +1,103 @@ +/** + * 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 + * + * 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 Log sinks and supporting resources. + +locals { + sink_bindings = { + for type in ["bigquery", "pubsub", "logging", "storage"] : + type => { + for name, sink in var.logging_sinks : + name => sink if sink.iam && sink.type == type + } + } +} + +resource "google_logging_project_sink" "sink" { + for_each = var.logging_sinks + name = each.key + description = coalesce(each.value.description, "${each.key} (Terraform-managed).") + project = local.project.project_id + destination = "${each.value.type}.googleapis.com/${each.value.destination}" + filter = each.value.filter + unique_writer_identity = each.value.unique_writer + disabled = each.value.disabled + + dynamic "bigquery_options" { + for_each = each.value.type == "biquery" && each.value.bq_partitioned_table != null ? [""] : [] + content { + use_partitioned_tables = each.value.bq_partitioned_table + } + } + + dynamic "exclusions" { + for_each = each.value.exclusions + iterator = exclusion + content { + name = exclusion.key + filter = exclusion.value + } + } + + depends_on = [ + google_project_iam_binding.authoritative, + google_project_iam_member.additive + ] +} + +resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { + for_each = local.sink_bindings["storage"] + bucket = each.value.destination + role = "roles/storage.objectCreator" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" { + for_each = local.sink_bindings["bigquery"] + project = split("/", each.value.destination)[1] + dataset_id = split("/", each.value.destination)[3] + role = "roles/bigquery.dataEditor" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" { + for_each = local.sink_bindings["pubsub"] + project = split("/", each.value.destination)[1] + topic = split("/", each.value.destination)[3] + role = "roles/pubsub.publisher" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_project_iam_member" "bucket-sinks-binding" { + for_each = local.sink_bindings["logging"] + project = split("/", each.value.destination)[1] + role = "roles/logging.bucketWriter" + member = google_logging_project_sink.sink[each.key].writer_identity + + condition { + title = "${each.key} bucket writer" + description = "Grants bucketWriter to ${google_logging_project_sink.sink[each.key].writer_identity} used by log sink ${each.key} on ${local.project.project_id}" + expression = "resource.name.endsWith('${each.value.destination}')" + } +} + +resource "google_logging_project_exclusion" "logging-exclusion" { + for_each = var.logging_exclusions + name = each.key + project = local.project.project_id + description = "${each.key} (Terraform-managed)." + filter = each.value +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/main.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/main.tf new file mode 100644 index 0000000..9f0dff4 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/main.tf @@ -0,0 +1,95 @@ +/** + * 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 + * + * 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 { + descriptive_name = ( + var.descriptive_name != null ? var.descriptive_name : "${local.prefix}${var.name}" + ) + parent_type = var.parent == null ? null : split("/", var.parent)[0] + parent_id = var.parent == null ? null : split("/", var.parent)[1] + prefix = var.prefix == null ? "" : "${var.prefix}-" + project = ( + var.project_create ? + { + project_id = try(google_project.project.0.project_id, null) + number = try(google_project.project.0.number, null) + name = try(google_project.project.0.name, null) + } + : { + project_id = "${local.prefix}${var.name}" + number = try(data.google_project.project.0.number, null) + name = try(data.google_project.project.0.name, null) + } + ) +} + +data "google_project" "project" { + count = var.project_create ? 0 : 1 + project_id = "${local.prefix}${var.name}" +} + +resource "google_project" "project" { + count = var.project_create ? 1 : 0 + org_id = local.parent_type == "organizations" ? local.parent_id : null + folder_id = local.parent_type == "folders" ? local.parent_id : null + project_id = "${local.prefix}${var.name}" + name = local.descriptive_name + billing_account = var.billing_account + auto_create_network = var.auto_create_network + labels = var.labels + skip_delete = var.skip_delete +} + +resource "google_project_service" "project_services" { + for_each = toset(var.services) + project = local.project.project_id + service = each.value + disable_on_destroy = var.service_config.disable_on_destroy + disable_dependent_services = var.service_config.disable_dependent_services +} + +resource "google_compute_project_metadata_item" "oslogin_meta" { + count = var.oslogin ? 1 : 0 + project = local.project.project_id + key = "enable-oslogin" + value = "TRUE" + # depend on services or it will fail on destroy + depends_on = [google_project_service.project_services] +} + +resource "google_resource_manager_lien" "lien" { + count = var.lien_reason != "" ? 1 : 0 + parent = "projects/${local.project.number}" + restrictions = ["resourcemanager.projects.delete"] + origin = "created-by-terraform" + reason = var.lien_reason +} + +resource "google_essential_contacts_contact" "contact" { + provider = google-beta + for_each = var.contacts + parent = "projects/${local.project.project_id}" + email = each.key + language_tag = "en" + notification_category_subscriptions = each.value +} + +resource "google_monitoring_monitored_project" "primary" { + provider = google-beta + for_each = toset(var.metric_scopes) + metrics_scope = each.value + name = local.project.project_id +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/organization-policies.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/organization-policies.tf new file mode 100644 index 0000000..7763aff --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/organization-policies.tf @@ -0,0 +1,142 @@ +/** + * 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 + * + * 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 Project-level organization policies. + +locals { + _factory_data_raw = merge([ + for f in try(fileset(var.org_policies_data_path, "*.yaml"), []) : + yamldecode(file("${var.org_policies_data_path}/${f}")) + ]...) + + # simulate applying defaults to data coming from yaml files + _factory_data = { + for k, v in local._factory_data_raw : + k => { + inherit_from_parent = try(v.inherit_from_parent, null) + reset = try(v.reset, null) + allow = can(v.allow) ? { + all = try(v.allow.all, null) + values = try(v.allow.values, null) + } : null + deny = can(v.deny) ? { + all = try(v.deny.all, null) + values = try(v.deny.values, null) + } : null + enforce = try(v.enforce, true) + + rules = [ + for r in try(v.rules, []) : { + allow = can(r.allow) ? { + all = try(r.allow.all, null) + values = try(r.allow.values, null) + } : null + deny = can(r.deny) ? { + all = try(r.deny.all, null) + values = try(r.deny.values, null) + } : null + enforce = try(r.enforce, true) + condition = { + description = try(r.condition.description, null) + expression = try(r.condition.expression, null) + location = try(r.condition.location, null) + title = try(r.condition.title, null) + } + } + ] + } + } + + _org_policies = merge(local._factory_data, var.org_policies) + + org_policies = { + for k, v in local._org_policies : + k => merge(v, { + name = "projects/${local.project.project_id}/policies/${k}" + parent = "projects/${local.project.project_id}" + + is_boolean_policy = v.allow == null && v.deny == null + has_values = ( + length(coalesce(try(v.allow.values, []), [])) > 0 || + length(coalesce(try(v.deny.values, []), [])) > 0 + ) + rules = [ + for r in v.rules : + merge(r, { + has_values = ( + length(coalesce(try(r.allow.values, []), [])) > 0 || + length(coalesce(try(r.deny.values, []), [])) > 0 + ) + }) + ] + }) + } +} + +resource "google_org_policy_policy" "default" { + for_each = local.org_policies + name = each.value.name + parent = each.value.parent + + spec { + inherit_from_parent = each.value.inherit_from_parent + reset = each.value.reset + + rules { + allow_all = try(each.value.allow.all, null) == true ? "TRUE" : null + deny_all = try(each.value.deny.all, null) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && each.value.enforce != null + ? upper(tostring(each.value.enforce)) + : null + ) + dynamic "values" { + for_each = each.value.has_values ? [1] : [] + content { + allowed_values = try(each.value.allow.values, null) + denied_values = try(each.value.deny.values, null) + } + } + } + + dynamic "rules" { + for_each = each.value.rules + iterator = rule + content { + allow_all = try(rule.value.allow.all, false) == true ? "TRUE" : null + deny_all = try(rule.value.deny.all, false) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && rule.value.enforce != null + ? upper(tostring(rule.value.enforce)) + : null + ) + condition { + description = rule.value.condition.description + expression = rule.value.condition.expression + location = rule.value.condition.location + title = rule.value.condition.title + } + dynamic "values" { + for_each = rule.value.has_values ? [1] : [] + content { + allowed_values = try(rule.value.allow.values, null) + denied_values = try(rule.value.deny.values, null) + } + } + } + } + } +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/outputs.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/outputs.tf new file mode 100644 index 0000000..cb940d0 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/outputs.tf @@ -0,0 +1,94 @@ +/** + * 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 + * + * 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. + */ + +output "custom_roles" { + description = "Ids of the created custom roles." + value = { + for name, role in google_project_iam_custom_role.roles : + name => role.id + } +} + +output "name" { + description = "Project name." + value = local.project.name + depends_on = [ + google_org_policy_policy.default, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "number" { + description = "Project number." + value = local.project.number + depends_on = [ + google_org_policy_policy.default, + google_project_service.project_services, + google_compute_shared_vpc_host_project.shared_vpc_host, + google_compute_shared_vpc_service_project.shared_vpc_service, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek, + google_project_service_identity.jit_si, + google_project_service_identity.servicenetworking, + google_project_iam_member.servicenetworking + ] +} + +output "project_id" { + description = "Project id." + value = "${local.prefix}${var.name}" + depends_on = [ + google_project.project, + data.google_project.project, + google_org_policy_policy.default, + google_project_service.project_services, + google_compute_shared_vpc_host_project.shared_vpc_host, + google_compute_shared_vpc_service_project.shared_vpc_service, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek, + google_project_service_identity.jit_si, + google_project_service_identity.servicenetworking, + google_project_iam_member.servicenetworking + ] +} + +output "service_accounts" { + description = "Product robot service accounts in project." + value = { + cloud_services = local.service_account_cloud_services + default = local.service_accounts_default + robots = local.service_accounts_robots + } + depends_on = [ + google_project_service.project_services, + google_kms_crypto_key_iam_member.service_identity_cmek, + google_project_service_identity.jit_si, + data.google_bigquery_default_service_account.bq_sa, + data.google_storage_project_service_account.gcs_sa + ] +} + +output "sink_writer_identities" { + description = "Writer identities created for each sink." + value = { + for name, sink in google_logging_project_sink.sink : name => sink.writer_identity + } +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/service-accounts.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/service-accounts.tf new file mode 100644 index 0000000..e1f6cb7 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/service-accounts.tf @@ -0,0 +1,152 @@ +/** + * 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 + * + * 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 Service identities and supporting resources. + +locals { + _service_accounts_cmek_service_dependencies = { + "composer" : [ + "composer", + "artifactregistry", "container-engine", "compute", "pubsub", "storage" + ] + "dataflow" : ["dataflow", "compute"] + } + _service_accounts_robot_services = { + apigee = "service-%s@gcp-sa-apigee" + artifactregistry = "service-%s@gcp-sa-artifactregistry" + bq = "bq-%s@bigquery-encryption" + cloudasset = "service-%s@gcp-sa-cloudasset" + cloudbuild = "service-%s@gcp-sa-cloudbuild" + cloudfunctions = "service-%s@gcf-admin-robot" + cloudrun = "service-%s@serverless-robot-prod" + composer = "service-%s@cloudcomposer-accounts" + compute = "service-%s@compute-system" + container-engine = "service-%s@container-engine-robot" + containerregistry = "service-%s@containerregistry" + dataflow = "service-%s@dataflow-service-producer-prod" + dataproc = "service-%s@dataproc-accounts" + fleet = "service-%s@gcp-sa-gkehub" + gae-flex = "service-%s@gae-api-prod" + # TODO: deprecate gcf + gcf = "service-%s@gcf-admin-robot" + # TODO: jit? + gke-mcs = "service-%s@gcp-sa-mcsd" + monitoring-notifications = "service-%s@gcp-sa-monitoring-notification" + pubsub = "service-%s@gcp-sa-pubsub" + secretmanager = "service-%s@gcp-sa-secretmanager" + sql = "service-%s@gcp-sa-cloud-sql" + sqladmin = "service-%s@gcp-sa-cloud-sql" + storage = "service-%s@gs-project-accounts" + } + service_accounts_default = { + compute = "${local.project.number}-compute@developer.gserviceaccount.com" + gae = "${local.project.project_id}@appspot.gserviceaccount.com" + } + service_account_cloud_services = ( + "${local.project.number}@cloudservices.gserviceaccount.com" + ) + service_accounts_robots = merge( + { + for k, v in local._service_accounts_robot_services : + k => "${format(v, local.project.number)}.iam.gserviceaccount.com" + }, + { + gke-mcs-importer = "${local.project.project_id}.svc.id.goog[gke-mcs/gke-mcs-importer]" + } + ) + service_accounts_jit_services = [ + "apigee.googleapis.com", + "artifactregistry.googleapis.com", + "cloudasset.googleapis.com", + "gkehub.googleapis.com", + "pubsub.googleapis.com", + "secretmanager.googleapis.com", + "sqladmin.googleapis.com" + ] + service_accounts_cmek_service_keys = distinct(flatten([ + for s in keys(var.service_encryption_key_ids) : [ + for ss in try(local._service_accounts_cmek_service_dependencies[s], [s]) : [ + for key in var.service_encryption_key_ids[s] : { + service = ss + key = key + } if key != null + ] + ] + ])) +} + +data "google_storage_project_service_account" "gcs_sa" { + count = contains(var.services, "storage.googleapis.com") ? 1 : 0 + project = local.project.project_id + depends_on = [google_project_service.project_services] +} + +data "google_bigquery_default_service_account" "bq_sa" { + count = contains(var.services, "bigquery.googleapis.com") ? 1 : 0 + project = local.project.project_id + depends_on = [google_project_service.project_services] +} + +resource "google_project_service_identity" "servicenetworking" { + provider = google-beta + count = contains(var.services, "servicenetworking.googleapis.com") ? 1 : 0 + project = local.project.project_id + service = "servicenetworking.googleapis.com" + depends_on = [google_project_service.project_services] +} + +resource "google_project_iam_member" "servicenetworking" { + count = contains(var.services, "servicenetworking.googleapis.com") ? 1 : 0 + project = local.project.project_id + role = "roles/servicenetworking.serviceAgent" + member = "serviceAccount:${google_project_service_identity.servicenetworking.0.email}" +} + +# Secret Manager SA created just in time, we need to trigger the creation. +resource "google_project_service_identity" "jit_si" { + for_each = setintersection(var.services, local.service_accounts_jit_services) + provider = google-beta + project = local.project.project_id + service = each.value + depends_on = [google_project_service.project_services] +} + +resource "google_kms_crypto_key_iam_member" "service_identity_cmek" { + for_each = { + for service_key in local.service_accounts_cmek_service_keys : + "${service_key.service}.${service_key.key}" => service_key + if service_key != service_key.key + } + crypto_key_id = each.value.key + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + member = "serviceAccount:${local.service_accounts_robots[each.value.service]}" + depends_on = [ + google_project.project, + google_project_service.project_services, + google_project_service_identity.jit_si, + data.google_bigquery_default_service_account.bq_sa, + data.google_project.project, + data.google_storage_project_service_account.gcs_sa, + ] +} + +resource "google_project_default_service_accounts" "default_service_accounts" { + count = upper(var.default_service_account) == "KEEP" ? 0 : 1 + action = upper(var.default_service_account) + project = local.project.project_id + restore_policy = "REVERT_AND_IGNORE_FAILURE" + depends_on = [google_project_service.project_services] +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/shared-vpc.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/shared-vpc.tf new file mode 100644 index 0000000..3894e5d --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/shared-vpc.tf @@ -0,0 +1,76 @@ +/** + * 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 + * + * 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 Shared VPC project-level configuration. + +locals { + # compute the host project IAM bindings for this project's service identities + _svpc_service_iam = flatten([ + for role, services in local._svpc_service_identity_iam : [ + for service in services : { role = role, service = service } + ] + ]) + _svpc_service_identity_iam = coalesce( + local.svpc_service_config.service_identity_iam, {} + ) + svpc_host_config = { + enabled = coalesce( + try(var.shared_vpc_host_config.enabled, null), false + ) + service_projects = coalesce( + try(var.shared_vpc_host_config.service_projects, null), [] + ) + } + svpc_service_config = coalesce(var.shared_vpc_service_config, { + host_project = null, service_identity_iam = {} + }) + svpc_service_iam = { + for b in local._svpc_service_iam : "${b.role}:${b.service}" => b + } +} + +resource "google_compute_shared_vpc_host_project" "shared_vpc_host" { + provider = google-beta + count = local.svpc_host_config.enabled ? 1 : 0 + project = local.project.project_id + depends_on = [google_project_service.project_services] +} + +resource "google_compute_shared_vpc_service_project" "service_projects" { + provider = google-beta + for_each = toset(local.svpc_host_config.service_projects) + host_project = local.project.project_id + service_project = each.value + depends_on = [google_compute_shared_vpc_host_project.shared_vpc_host] +} + +resource "google_compute_shared_vpc_service_project" "shared_vpc_service" { + provider = google-beta + count = local.svpc_service_config.host_project != null ? 1 : 0 + host_project = var.shared_vpc_service_config.host_project + service_project = local.project.project_id +} + +resource "google_project_iam_member" "shared_vpc_host_robots" { + for_each = local.svpc_service_iam + project = var.shared_vpc_service_config.host_project + role = each.value.role + member = ( + each.value.service == "cloudservices" + ? "serviceAccount:${local.service_account_cloud_services}" + : "serviceAccount:${local.service_accounts_robots[each.value.service]}" + ) +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/tags.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/tags.tf new file mode 100644 index 0000000..683143b --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/tags.tf @@ -0,0 +1,21 @@ +/** + * 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 + * + * 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. + */ + +resource "google_tags_tag_binding" "binding" { + for_each = coalesce(var.tag_bindings, {}) + parent = "//cloudresourcemanager.googleapis.com/projects/${local.project.number}" + tag_value = each.value +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/variables.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/variables.tf new file mode 100644 index 0000000..3769a1f --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/variables.tf @@ -0,0 +1,305 @@ +/** + * 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 + * + * 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. + */ + +variable "auto_create_network" { + description = "Whether to create the default network for the project." + type = bool + default = false +} + +variable "billing_account" { + description = "Billing account id." + type = string + default = null +} + +variable "contacts" { + description = "List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES." + type = map(list(string)) + default = {} + nullable = false +} + +variable "custom_roles" { + description = "Map of role name => list of permissions to create in this project." + type = map(list(string)) + default = {} + nullable = false +} + +variable "default_service_account" { + description = "Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`." + default = "keep" + type = string +} + +variable "descriptive_name" { + description = "Name of the project name. Used for project name instead of `name` variable." + type = string + default = null +} + +variable "group_iam" { + description = "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." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam" { + description = "IAM bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_additive" { + description = "IAM additive bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +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 "labels" { + description = "Resource labels." + type = map(string) + default = {} + nullable = false +} + +variable "lien_reason" { + description = "If non-empty, creates a project lien with this description." + type = string + default = "" +} + +variable "logging_exclusions" { + description = "Logging exclusions for this project in the form {NAME -> FILTER}." + type = map(string) + default = {} + nullable = false +} + +variable "logging_sinks" { + description = "Logging sinks to create for this project." + type = map(object({ + bq_partitioned_table = optional(bool) + description = optional(string) + destination = string + disabled = optional(bool, false) + exclusions = optional(map(string), {}) + filter = string + iam = optional(bool, true) + type = string + unique_writer = optional(bool) + })) + default = {} + nullable = false + validation { + condition = alltrue([ + for k, v in var.logging_sinks : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } + validation { + condition = alltrue([ + for k, v in var.logging_sinks : + v.bq_partitioned_table != true || v.type == "bigquery" + ]) + error_message = "Can only set bq_partitioned_table when type is `bigquery`." + } +} + +variable "metric_scopes" { + description = "List of projects that will act as metric scopes for this project." + type = list(string) + default = [] + nullable = false +} + +variable "name" { + description = "Project name and id suffix." + type = string +} + +variable "org_policies" { + description = "Organization policies applied to this project keyed by policy name." + type = map(object({ + inherit_from_parent = optional(bool) # for list policies only. + reset = optional(bool) + + # default (unconditional) values + allow = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + deny = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + enforce = optional(bool, true) # for boolean policies only. + + # conditional values + 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, true) # for boolean policies only. + condition = object({ + description = optional(string) + expression = optional(string) + location = optional(string) + title = optional(string) + }) + })), []) + })) + default = {} + nullable = false +} + +variable "org_policies_data_path" { + description = "Path containing org policies in YAML format." + type = string + default = null +} + +variable "oslogin" { + description = "Enable OS Login." + type = bool + default = false +} + +variable "oslogin_admins" { + description = "List of IAM-style identities that will be granted roles necessary for OS Login administrators." + type = list(string) + default = [] + nullable = false + +} + +variable "oslogin_users" { + description = "List of IAM-style identities that will be granted roles necessary for OS Login users." + type = list(string) + default = [] + nullable = false +} + +variable "parent" { + description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "prefix" { + description = "Optional prefix used to generate project id and name." + type = string + default = null + validation { + condition = var.prefix != "" + error_message = "Prefix cannot be empty, please use null instead." + } +} + +variable "project_create" { + description = "Create project. When set to false, uses a data source to reference existing project." + type = bool + default = true +} + +variable "service_config" { + description = "Configure service API activation." + type = object({ + disable_on_destroy = bool + disable_dependent_services = bool + }) + default = { + disable_on_destroy = false + disable_dependent_services = false + } +} + +variable "service_encryption_key_ids" { + description = "Cloud KMS encryption key in {SERVICE => [KEY_URL]} format." + type = map(list(string)) + default = {} +} + +# accessPolicies/ACCESS_POLICY_NAME/servicePerimeters/PERIMETER_NAME +variable "service_perimeter_bridges" { + description = "Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format." + type = list(string) + default = null +} + +# accessPolicies/ACCESS_POLICY_NAME/servicePerimeters/PERIMETER_NAME +variable "service_perimeter_standard" { + description = "Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format." + type = string + default = null +} + +variable "services" { + description = "Service APIs to enable." + type = list(string) + default = [] +} + +variable "shared_vpc_host_config" { + description = "Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project)." + type = object({ + enabled = bool + service_projects = optional(list(string), []) + }) + default = null +} + +variable "shared_vpc_service_config" { + description = "Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config)." + # the list of valid service identities is in service-accounts.tf + type = object({ + host_project = string + service_identity_iam = optional(map(list(string))) + }) + default = null +} + +variable "skip_delete" { + description = "Allows the underlying resources to be destroyed without destroying the project itself." + type = bool + default = false +} + +variable "tag_bindings" { + description = "Tag bindings for this project, in key => tag value id format." + type = map(string) + default = null +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/versions.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/versions.tf new file mode 100644 index 0000000..286536a --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.3.1" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.40.0" # tftest + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.40.0" # tftest + } + } +} + + diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/vpc-sc.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/vpc-sc.tf new file mode 100644 index 0000000..edaa203 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/project/vpc-sc.tf @@ -0,0 +1,45 @@ +/** + * 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 + * + * 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 VPC-SC project-level perimeter configuration. + +moved { + from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-standard + to = google_access_context_manager_service_perimeter_resource.standard +} + +resource "google_access_context_manager_service_perimeter_resource" "standard" { + count = var.service_perimeter_standard != null ? 1 : 0 + # this needs an additional lifecycle block in the vpc module on the + # google_access_context_manager_service_perimeter resource + perimeter_name = var.service_perimeter_standard + resource = "projects/${local.project.number}" +} + +moved { + from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-bridges + to = google_access_context_manager_service_perimeter_resource.bridge +} + +resource "google_access_context_manager_service_perimeter_resource" "bridge" { + for_each = toset( + var.service_perimeter_bridges != null ? var.service_perimeter_bridges : [] + ) + # this needs an additional lifecycle block in the vpc module on the + # google_access_context_manager_service_perimeter resource + perimeter_name = each.value + resource = "projects/${local.project.number}" +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/terraform.auto.tfvars b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/terraform.auto.tfvars new file mode 100644 index 0000000..e8b106d --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/terraform.auto.tfvars @@ -0,0 +1,24 @@ +# 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. + + +parent = "folders/704617814954" +project_id = "iac-prd-v2" +tfe_organization_id = "org-78zP2BW2RbKAGWVZ" +#tfe_workspace_id = "ws-XYWD4kkUxVFuFadG" +#tfe_workspace_id = "ws-Jy32aUtnnCDA37ZJ" +#tfe_workspace_id = "ws-jpHPtFwszCV8X6K9" # Folder-Factory +tfe_workspace_id = "ws-LZYtvMqtqNuSDPeU" # Folder-Factory-GUI Correct one +#tfe_workspace_id = "ws-fDhVEARWPfKN2dpM" Project Factory GUI for Testing Only +billing_account = "019609-F059A9-76DC20" diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/terraform.auto.tfvars.template b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/terraform.auto.tfvars.template new file mode 100644 index 0000000..9430677 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/terraform.auto.tfvars.template @@ -0,0 +1,20 @@ +# 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. + + +parent = "folders/ 555271503501" +project_id = "iac-prd-371009" +tfe_organization_id = "org-78zP2BW2RbKAGWVZ" +tfe_workspace_id = "ws-DFxEE3NmeMdaAvoK" +billing_account = "ws-3jrbXyP9CrL4xtCN" diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/terraform.tfstate b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/terraform.tfstate new file mode 100644 index 0000000..855ef5c --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/terraform.tfstate @@ -0,0 +1,219 @@ +{ + "version": 4, + "terraform_version": "1.3.6", + "serial": 21, + "lineage": "ce70fc23-c57f-91a1-c9f9-6ffd34cb29ce", + "outputs": { + "impersonate_service_account_email": { + "value": "sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "type": "string" + }, + "project_id": { + "value": "iac-prd-v2", + "type": "string" + }, + "workload_identity_audience": { + "value": "//iam.googleapis.com/projects/355501319207/locations/global/workloadIdentityPools/tfe-pool-ff-v1/providers/tfe-provider-ff", + "type": "string" + }, + "workload_identity_pool_provider_id": { + "value": "projects/355501319207/locations/global/workloadIdentityPools/tfe-pool-ff-v1/providers/tfe-provider-ff", + "type": "string" + } + }, + "resources": [ + { + "mode": "managed", + "type": "google_iam_workload_identity_pool", + "name": "tfe-pool-ff", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "description": "Identity pool for Terraform Enterprise OIDC integration", + "disabled": false, + "display_name": "TFE Pool ff", + "id": "projects/iac-prd-v2/locations/global/workloadIdentityPools/tfe-pool-ff-v1", + "name": "projects/355501319207/locations/global/workloadIdentityPools/tfe-pool-ff-v1", + "project": "iac-prd-v2", + "state": "ACTIVE", + "timeouts": null, + "workload_identity_pool_id": "tfe-pool-ff-v1" + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19" + } + ] + }, + { + "mode": "managed", + "type": "google_iam_workload_identity_pool_provider", + "name": "tfe-pool-provider", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "attribute_condition": "attribute.terraform_organization_id == \"org-78zP2BW2RbKAGWVZ\"", + "attribute_mapping": { + "attribute.aud": "assertion.aud", + "attribute.terraform_full_workspace": "assertion.terraform_full_workspace", + "attribute.terraform_organization_id": "assertion.terraform_organization_id", + "attribute.terraform_organization_name": "assertion.terraform_organization_name", + "attribute.terraform_run_id": "assertion.terraform_run_id", + "attribute.terraform_run_phase": "assertion.terraform_run_phase", + "attribute.terraform_workspace_id": "assertion.terraform_workspace_id", + "attribute.terraform_workspace_name": "assertion.terraform_workspace_name", + "google.subject": "assertion.sub" + }, + "aws": [], + "description": "OIDC identity pool provider for TFE Integration ff", + "disabled": false, + "display_name": "TFE Pool Provider ff", + "id": "projects/iac-prd-v2/locations/global/workloadIdentityPools/tfe-pool-ff-v1/providers/tfe-provider-ff", + "name": "projects/355501319207/locations/global/workloadIdentityPools/tfe-pool-ff-v1/providers/tfe-provider-ff", + "oidc": [ + { + "allowed_audiences": [], + "issuer_uri": "https://app.terraform.io/" + } + ], + "project": "iac-prd-v2", + "state": "ACTIVE", + "timeouts": null, + "workload_identity_pool_id": "tfe-pool-ff-v1", + "workload_identity_pool_provider_id": "tfe-provider-ff" + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19", + "dependencies": [ + "google_iam_workload_identity_pool.tfe-pool-ff" + ] + } + ] + }, + { + "module": "module.sa-tfe", + "mode": "managed", + "type": "google_project_iam_member", + "name": "project-roles", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "index_key": "iac-prd-v2-roles/compute.networkAdmin", + "schema_version": 0, + "attributes": { + "condition": [], + "etag": "BwXwZNNZ9Uk=", + "id": "iac-prd-v2/roles/compute.networkAdmin/serviceAccount:sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "member": "serviceAccount:sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "project": "iac-prd-v2", + "role": "roles/compute.networkAdmin" + }, + "sensitive_attributes": [], + "private": "bnVsbA==", + "dependencies": [ + "module.sa-tfe.data.google_service_account.service_account", + "module.sa-tfe.google_service_account.service_account" + ] + }, + { + "index_key": "iac-prd-v2-roles/compute.networkUser", + "schema_version": 0, + "attributes": { + "condition": [], + "etag": "BwXwZNNZ9Uk=", + "id": "iac-prd-v2/roles/compute.networkUser/serviceAccount:sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "member": "serviceAccount:sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "project": "iac-prd-v2", + "role": "roles/compute.networkUser" + }, + "sensitive_attributes": [], + "private": "bnVsbA==", + "dependencies": [ + "module.sa-tfe.data.google_service_account.service_account", + "module.sa-tfe.google_service_account.service_account" + ] + }, + { + "index_key": "iac-prd-v2-roles/storage.admin", + "schema_version": 0, + "attributes": { + "condition": [], + "etag": "BwXwZNNZ9Uk=", + "id": "iac-prd-v2/roles/storage.admin/serviceAccount:sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "member": "serviceAccount:sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "project": "iac-prd-v2", + "role": "roles/storage.admin" + }, + "sensitive_attributes": [], + "private": "bnVsbA==", + "dependencies": [ + "module.sa-tfe.data.google_service_account.service_account", + "module.sa-tfe.google_service_account.service_account" + ] + } + ] + }, + { + "module": "module.sa-tfe", + "mode": "managed", + "type": "google_service_account", + "name": "service_account", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "index_key": 0, + "schema_version": 0, + "attributes": { + "account_id": "sa-tfe-ff", + "description": "", + "disabled": false, + "display_name": "Terraform-managed.", + "email": "sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "id": "projects/iac-prd-v2/serviceAccounts/sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "member": "serviceAccount:sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "name": "projects/iac-prd-v2/serviceAccounts/sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "project": "iac-prd-v2", + "timeouts": null, + "unique_id": "111247314115780800707" + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMDB9fQ==" + } + ] + }, + { + "module": "module.sa-tfe", + "mode": "managed", + "type": "google_service_account_iam_binding", + "name": "roles", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "index_key": "roles/iam.workloadIdentityUser", + "schema_version": 0, + "attributes": { + "condition": [], + "etag": "BwXxQf2LBlQ=", + "id": "projects/iac-prd-v2/serviceAccounts/sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com/roles/iam.workloadIdentityUser", + "members": [ + "principalSet://iam.googleapis.com/projects/355501319207/locations/global/workloadIdentityPools/tfe-pool-ff-v1/attribute.terraform_workspace_id/ws-LZYtvMqtqNuSDPeU" + ], + "role": "roles/iam.workloadIdentityUser", + "service_account_id": "projects/iac-prd-v2/serviceAccounts/sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com" + }, + "sensitive_attributes": [], + "private": "bnVsbA==", + "dependencies": [ + "google_iam_workload_identity_pool.tfe-pool-ff", + "module.sa-tfe.data.google_service_account.service_account", + "module.sa-tfe.google_service_account.service_account" + ] + } + ] + } + ], + "check_results": null +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/terraform.tfstate.backup b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/terraform.tfstate.backup new file mode 100644 index 0000000..9d10e13 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/terraform.tfstate.backup @@ -0,0 +1,219 @@ +{ + "version": 4, + "terraform_version": "1.3.6", + "serial": 19, + "lineage": "ce70fc23-c57f-91a1-c9f9-6ffd34cb29ce", + "outputs": { + "impersonate_service_account_email": { + "value": "sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "type": "string" + }, + "project_id": { + "value": "iac-prd-v2", + "type": "string" + }, + "workload_identity_audience": { + "value": "//iam.googleapis.com/projects/355501319207/locations/global/workloadIdentityPools/tfe-pool-ff-v1/providers/tfe-provider-ff", + "type": "string" + }, + "workload_identity_pool_provider_id": { + "value": "projects/355501319207/locations/global/workloadIdentityPools/tfe-pool-ff-v1/providers/tfe-provider-ff", + "type": "string" + } + }, + "resources": [ + { + "mode": "managed", + "type": "google_iam_workload_identity_pool", + "name": "tfe-pool-ff", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "description": "Identity pool for Terraform Enterprise OIDC integration", + "disabled": false, + "display_name": "TFE Pool ff", + "id": "projects/iac-prd-v2/locations/global/workloadIdentityPools/tfe-pool-ff-v1", + "name": "projects/355501319207/locations/global/workloadIdentityPools/tfe-pool-ff-v1", + "project": "iac-prd-v2", + "state": "ACTIVE", + "timeouts": null, + "workload_identity_pool_id": "tfe-pool-ff-v1" + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19" + } + ] + }, + { + "mode": "managed", + "type": "google_iam_workload_identity_pool_provider", + "name": "tfe-pool-provider", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "attribute_condition": "attribute.terraform_organization_id == \"org-78zP2BW2RbKAGWVZ\"", + "attribute_mapping": { + "attribute.aud": "assertion.aud", + "attribute.terraform_full_workspace": "assertion.terraform_full_workspace", + "attribute.terraform_organization_id": "assertion.terraform_organization_id", + "attribute.terraform_organization_name": "assertion.terraform_organization_name", + "attribute.terraform_run_id": "assertion.terraform_run_id", + "attribute.terraform_run_phase": "assertion.terraform_run_phase", + "attribute.terraform_workspace_id": "assertion.terraform_workspace_id", + "attribute.terraform_workspace_name": "assertion.terraform_workspace_name", + "google.subject": "assertion.sub" + }, + "aws": [], + "description": "OIDC identity pool provider for TFE Integration ff", + "disabled": false, + "display_name": "TFE Pool Provider ff", + "id": "projects/iac-prd-v2/locations/global/workloadIdentityPools/tfe-pool-ff-v1/providers/tfe-provider-ff", + "name": "projects/355501319207/locations/global/workloadIdentityPools/tfe-pool-ff-v1/providers/tfe-provider-ff", + "oidc": [ + { + "allowed_audiences": null, + "issuer_uri": "https://app.terraform.io/" + } + ], + "project": "iac-prd-v2", + "state": "ACTIVE", + "timeouts": null, + "workload_identity_pool_id": "tfe-pool-ff-v1", + "workload_identity_pool_provider_id": "tfe-provider-ff" + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19", + "dependencies": [ + "google_iam_workload_identity_pool.tfe-pool-ff" + ] + } + ] + }, + { + "module": "module.sa-tfe", + "mode": "managed", + "type": "google_project_iam_member", + "name": "project-roles", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "index_key": "iac-prd-v2-roles/compute.networkAdmin", + "schema_version": 0, + "attributes": { + "condition": [], + "etag": "BwXwZNNZ9Uk=", + "id": "iac-prd-v2/roles/compute.networkAdmin/serviceAccount:sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "member": "serviceAccount:sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "project": "iac-prd-v2", + "role": "roles/compute.networkAdmin" + }, + "sensitive_attributes": [], + "private": "bnVsbA==", + "dependencies": [ + "module.sa-tfe.data.google_service_account.service_account", + "module.sa-tfe.google_service_account.service_account" + ] + }, + { + "index_key": "iac-prd-v2-roles/compute.networkUser", + "schema_version": 0, + "attributes": { + "condition": [], + "etag": "BwXwZNNZ9Uk=", + "id": "iac-prd-v2/roles/compute.networkUser/serviceAccount:sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "member": "serviceAccount:sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "project": "iac-prd-v2", + "role": "roles/compute.networkUser" + }, + "sensitive_attributes": [], + "private": "bnVsbA==", + "dependencies": [ + "module.sa-tfe.data.google_service_account.service_account", + "module.sa-tfe.google_service_account.service_account" + ] + }, + { + "index_key": "iac-prd-v2-roles/storage.admin", + "schema_version": 0, + "attributes": { + "condition": [], + "etag": "BwXwZNNZ9Uk=", + "id": "iac-prd-v2/roles/storage.admin/serviceAccount:sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "member": "serviceAccount:sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "project": "iac-prd-v2", + "role": "roles/storage.admin" + }, + "sensitive_attributes": [], + "private": "bnVsbA==", + "dependencies": [ + "module.sa-tfe.data.google_service_account.service_account", + "module.sa-tfe.google_service_account.service_account" + ] + } + ] + }, + { + "module": "module.sa-tfe", + "mode": "managed", + "type": "google_service_account", + "name": "service_account", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "index_key": 0, + "schema_version": 0, + "attributes": { + "account_id": "sa-tfe-ff", + "description": "", + "disabled": false, + "display_name": "Terraform-managed.", + "email": "sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "id": "projects/iac-prd-v2/serviceAccounts/sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "member": "serviceAccount:sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "name": "projects/iac-prd-v2/serviceAccounts/sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com", + "project": "iac-prd-v2", + "timeouts": null, + "unique_id": "111247314115780800707" + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozMDAwMDAwMDAwMDB9fQ==" + } + ] + }, + { + "module": "module.sa-tfe", + "mode": "managed", + "type": "google_service_account_iam_binding", + "name": "roles", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "index_key": "roles/iam.workloadIdentityUser", + "schema_version": 0, + "attributes": { + "condition": [], + "etag": "BwXwZNOsosE=", + "id": "projects/iac-prd-v2/serviceAccounts/sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com/roles/iam.workloadIdentityUser", + "members": [ + "principalSet://iam.googleapis.com/projects/355501319207/locations/global/workloadIdentityPools/tfe-pool-ff-v1/attribute.terraform_workspace_id/ws-jpHPtFwszCV8X6K9" + ], + "role": "roles/iam.workloadIdentityUser", + "service_account_id": "projects/iac-prd-v2/serviceAccounts/sa-tfe-ff@iac-prd-v2.iam.gserviceaccount.com" + }, + "sensitive_attributes": [], + "private": "bnVsbA==", + "dependencies": [ + "google_iam_workload_identity_pool.tfe-pool-ff", + "module.sa-tfe.data.google_service_account.service_account", + "module.sa-tfe.google_service_account.service_account" + ] + } + ] + } + ], + "check_results": null +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/variables.tf b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/variables.tf new file mode 100644 index 0000000..2a865af --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/variables.tf @@ -0,0 +1,68 @@ +# 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. + + +variable "billing_account" { + description = "Billing account id used as default for new projects." + type = string +} + +variable "issuer_uri" { + description = "Terraform Enterprise uri. Replace the uri if a self hosted instance is used." + type = string + default = "https://app.terraform.io/" +} + +variable "parent" { + description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "project_create" { + description = "Create project instead of using an existing one." + type = bool + default = true +} + +variable "project_id" { + description = "Existing project id." + type = string +} + +variable "tfe_organization_id" { + description = "TFE organization id." + type = string +} + +variable "tfe_workspace_id" { + description = "TFE workspace id." + type = string +} + +variable "workload_identity_pool_id" { + description = "Workload identity pool id." + type = string + default = "tfe-pool-ff-v1" +} + +variable "workload_identity_pool_provider_id" { + description = "Workload identity pool provider id." + type = string + default = "tfe-provider-ff" +} diff --git a/examples/guardrails/terraform-cloud/folder-factory/tfc-oidc/README.md b/examples/guardrails/terraform-cloud/folder-factory/tfc-oidc/README.md new file mode 100644 index 0000000..534d659 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/tfc-oidc/README.md @@ -0,0 +1,38 @@ +# Terraform Enterprise OIDC Credential for GCP Workload Identity Federation + +This is a helper module to prepare GCP Credentials from Terraform Enterprise workload identity token. For more information see [Terraform Enterprise Workload Identity Federation](../) blueprint. + +## Example +```hcl +module "tfe_oidc" { + source = "./tfc-oidc" + + impersonate_service_account_email = "tfe-test@tfe-test-wif.iam.gserviceaccount.com" +} + +provider "google" { + credentials = module.tfe_oidc.credentials +} + +provider "google-beta" { + credentials = module.tfe_oidc.credentials +} + +# tftest skip +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [impersonate_service_account_email](variables.tf#L17) | Service account to be impersonated by workload identity federation. | string | ✓ | | +| [tmp_oidc_token_path](variables.tf#L22) | Name of the temporary file where TFC OIDC token will be stored to authentificate terraform provider google. | string | | ".oidc_token" | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [credentials](outputs.tf#L17) | Credentials in format to pass the to gcp provider. | | + + diff --git a/examples/guardrails/terraform-cloud/folder-factory/tfc-oidc/get_audience.sh b/examples/guardrails/terraform-cloud/folder-factory/tfc-oidc/get_audience.sh new file mode 100644 index 0000000..251fe32 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/tfc-oidc/get_audience.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# 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. + +# Exit if any of the intermediate steps fail +set -e + +cat < $FILENAME + +echo -n "{\"file\":\"${FILENAME}\"}" diff --git a/examples/guardrails/terraform-cloud/folder-factory/variables.tf b/examples/guardrails/terraform-cloud/folder-factory/variables.tf new file mode 100644 index 0000000..914e428 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/variables.tf @@ -0,0 +1,25 @@ +/** + * 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 + * + * 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. + */ + + variable "impersonate_service_account_email" { + description = "Service account to be impersonated by workload identity." + type = string +} + +# variable "project_id" { +# description = "GCP project ID." +# type = string +# } diff --git a/examples/guardrails/terraform-cloud/project-factory/.github/workflows/terraform-deployment.yml b/examples/guardrails/terraform-cloud/project-factory/.github/workflows/terraform-deployment.yml new file mode 100644 index 0000000..4099678 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/.github/workflows/terraform-deployment.yml @@ -0,0 +1,94 @@ +# 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 +# +# 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. + +name: 'Cloud Deployment' + +on: + push: + branches: + - main + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + FOLDER: ${{ secrets.FOLDER }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} + +# OR SET MANUALLY +# +#env: +# STATE_BUCKET: 'XXXX' +# FOLDER: 'folders/XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan -var "folder=$FOLDER" + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -var "folder=$FOLDER" -auto-approve + + diff --git a/examples/guardrails/terraform-cloud/project-factory/.gitignore b/examples/guardrails/terraform-cloud/project-factory/.gitignore new file mode 100644 index 0000000..a5d8c0e --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.terraform +.terraform.lock.hcl diff --git a/examples/guardrails/terraform-cloud/project-factory/README.md b/examples/guardrails/terraform-cloud/project-factory/README.md new file mode 100644 index 0000000..a8b2f38 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/README.md @@ -0,0 +1,80 @@ +# Project Factory + +This is a template for a DevOps project factory. + +It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory) and is intended to house the projects of a specified folder: + +Overview + +Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. + +![Folder Factory](https://user-images.githubusercontent.com/94000358/169809882-f5ff9fb1-d037-49de-8c2c-bf0d457b662f.png) + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +## Setting Up Terraform Wokspace on Terraform Cloud + +Ensure to have a Workspace created on terraform Cloud which would have Gitlab Repository as the VCS Source + +Update the variables for Terraform Workspace as below + +``` +env: + impersonate_service_account_email: 'xxx@project.iam.gserviceaccount.com' + # The Service Account used to create Folder + + folder: 'xxxx' + # Folder under which Projects will be created + + TFC_WORKLOAD_IDENTITY_AUDIENCE: '//iam.googleapis.com/projects/id/locations/global/workloadIdentityPools//providers/' + # WorkLoad Identity Audience will be used by tfc-oidc module for token generation and impersonation +``` + + +> **_NOTE:_** You need to have TFC Workspace ID created manually, before it can be passed in terraform-cloud-wif module under Folder Factory to generate the Provider, Pool Service account and IAM Role attached to the role. + +## Setting up projects + +The project factory will: +- create a service account with defined rights +- create a project within the folder +- connect the service account to the Github repository informantion + +It uses YAML configuration files for every project with the following sample structure: +``` +billing_account_id: XXXXXX-XXXXXX-XXXXXX +roles: + - roles/viewer + - roles/iam.serviceAccountUser + - roles/iam.securityReviewer + - roles/monitoring.viewer + - roles/monitoring.editor + - roles/monitoring.alertPolicyViewer + - roles/monitoring.alertPolicyEditor + - roles/monitoring.dashboardViewer + - roles/monitoring.dashboardEditor + - roles/monitoring.notificationChannelViewer + - roles/monitoring.notificationChannelEditor + - roles/monitoring.servicesViewer + - roles/monitoring.servicesEditor + - roles/monitoring.uptimeCheckConfigViewer + - roles/monitoring.uptimeCheckConfigEditor + - roles/secretmanager.viewer + - roles/secretmanager.secretVersionManager + - roles/secretmanager.admin + - roles/storage.admin + - roles/storage.objectAdmin + - roles/storage.objectCreator + - roles/storage.objectViewer +repo_provider: gitlab +tfe_workspace_id: ws-xxxx +``` + +Every project is defined with its own file located in the [Project Folder](data/projects). + +> **_NOTE:_** You can also manage the environments seprately via a diffrent Gitlab Branches for each Environment Which and having environment specific file under [Project Folder](data/projects). These branches can be tied to individual workspace. \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/project-factory/data/projects/dev-skunkworks.yaml b/examples/guardrails/terraform-cloud/project-factory/data/projects/dev-skunkworks.yaml new file mode 100644 index 0000000..6a12bff --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/data/projects/dev-skunkworks.yaml @@ -0,0 +1,42 @@ +# 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 +# +# 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.. + +billing_account_id: 019609-F059A9-76DC20 +# folder: folders/872171141499 +roles: + - roles/viewer + - roles/iam.serviceAccountUser + - roles/iam.securityReviewer + - roles/monitoring.viewer + - roles/monitoring.editor + - roles/monitoring.alertPolicyViewer + - roles/monitoring.alertPolicyEditor + - roles/monitoring.dashboardViewer + - roles/monitoring.dashboardEditor + - roles/monitoring.notificationChannelViewer + - roles/monitoring.notificationChannelEditor + - roles/monitoring.servicesViewer + - roles/monitoring.servicesEditor + - roles/monitoring.uptimeCheckConfigViewer + - roles/monitoring.uptimeCheckConfigEditor + - roles/secretmanager.viewer + - roles/secretmanager.secretVersionManager + - roles/secretmanager.admin + - roles/storage.admin + - roles/storage.objectAdmin + - roles/storage.objectCreator + - roles/storage.objectViewer +repo_provider: gitlab +#tfe_organization_id: org-78zP2BW2RbKAGWVZ +tfe_workspace_id: ws-JttC5tTBrmbTrMrR \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/project-factory/data/projects/prod1-skunkworks.yaml b/examples/guardrails/terraform-cloud/project-factory/data/projects/prod1-skunkworks.yaml new file mode 100644 index 0000000..9ed271f --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/data/projects/prod1-skunkworks.yaml @@ -0,0 +1,42 @@ +# 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 +# +# 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. + +billing_account_id: 019609-F059A9-76DC20 +# folder: folders/351770012667 +roles: + - roles/viewer + - roles/iam.serviceAccountUser + - roles/iam.securityReviewer + - roles/monitoring.viewer + - roles/monitoring.editor + - roles/monitoring.alertPolicyViewer + - roles/monitoring.alertPolicyEditor + - roles/monitoring.dashboardViewer + - roles/monitoring.dashboardEditor + - roles/monitoring.notificationChannelViewer + - roles/monitoring.notificationChannelEditor + - roles/monitoring.servicesViewer + - roles/monitoring.servicesEditor + - roles/monitoring.uptimeCheckConfigViewer + - roles/monitoring.uptimeCheckConfigEditor + - roles/secretmanager.viewer + - roles/secretmanager.secretVersionManager + - roles/secretmanager.admin + - roles/storage.admin + - roles/storage.objectAdmin + - roles/storage.objectCreator + - roles/storage.objectViewer +repo_provider: gitlab +#tfe_organization_id: org-78zP2BW2RbKAGWVZ +tfe_workspace_id: ws-AEdYtyNfFdA8Vwy4 \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/project-factory/data/projects/stage-skunkworks.yaml.template b/examples/guardrails/terraform-cloud/project-factory/data/projects/stage-skunkworks.yaml.template new file mode 100644 index 0000000..a48963f --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/data/projects/stage-skunkworks.yaml.template @@ -0,0 +1,43 @@ +# 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 +# +# 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. + +billing_account_id: 01CDF0-32B3CB-6AF895 +# folder: folders/872171141499 +roles: + - roles/viewer + - roles/iam.serviceAccountUser + - roles/iam.securityReviewer + - roles/monitoring.viewer + - roles/monitoring.editor + - roles/monitoring.alertPolicyViewer + - roles/monitoring.alertPolicyEditor + - roles/monitoring.dashboardViewer + - roles/monitoring.dashboardEditor + - roles/monitoring.notificationChannelViewer + - roles/monitoring.notificationChannelEditor + - roles/monitoring.servicesViewer + - roles/monitoring.servicesEditor + - roles/monitoring.uptimeCheckConfigViewer + - roles/monitoring.uptimeCheckConfigEditor + - roles/secretmanager.viewer + - roles/secretmanager.secretVersionManager + - roles/secretmanager.admin + - roles/storage.admin + - roles/storage.objectAdmin + - roles/storage.objectCreator + - roles/storage.objectViewer + +repo_provider: gitlab +tfe_organization_id: org-78zP2BW2RbKAGWVZ +tfe_workspace_id: ws-fDhVEARWPfKN2dpM \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/project-factory/main.tf b/examples/guardrails/terraform-cloud/project-factory/main.tf new file mode 100644 index 0000000..b01ae21 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/main.tf @@ -0,0 +1,39 @@ +/** + * 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 + * + * 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. + */ + +#QUERY: Consider creating a state bucket in the project as well along with the project creation? Use the same bucket as folder factory. + +# Enable iamcredentials.googlepais.com service on the newly created project ? it is a prerequisitie for the WIF to work. + +locals { + projects = { + for f in fileset("./data/projects", "**/*.yaml") : + trimsuffix(f, ".yaml") => yamldecode(file("./data/projects/${f}")) + } +} + +module "project" { + source = "./modules/project_plus" + for_each = local.projects + team = each.key + billing_account = each.value.billing_account_id + folder = var.folder + roles = try(each.value.roles, []) + tfe_workspace_id = each.value.tfe_workspace_id + #wif-pool = each.value.repo_provider == "gitlab" ? google_iam_workload_identity_pool.wif-pool-gitlab.name : google_iam_workload_identity_pool.wif-pool-github.name + wif-pool = google_iam_workload_identity_pool.wif-pool-gitlab.name + depends_on = [google_iam_workload_identity_pool.wif-pool-gitlab] +} diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project/README.md b/examples/guardrails/terraform-cloud/project-factory/modules/project/README.md new file mode 100644 index 0000000..e4f2139 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project/README.md @@ -0,0 +1,308 @@ +# Project Module + +## Examples + +### Minimal example with IAM + +```hcl +locals { + gke_service_account = "my_gke_service_account" +} + +module "project" { + source = "./modules/project" + billing_account = "123456-123456-123456" + name = "project-example" + parent = "folders/1234567890" + prefix = "foo" + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + iam = { + "roles/container.hostServiceAgentUser" = [ + "serviceAccount:${local.gke_service_account}" + ] + } +} +# tftest modules=1 resources=4 +``` + +### Minimal example with IAM additive roles + +```hcl +module "project" { + source = "./modules/project" + name = "project-example" + + iam_additive = { + "roles/viewer" = [ + "group:one@example.org", "group:two@xample.org" + ], + "roles/storage.objectAdmin" = [ + "group:two@example.org" + ], + "roles/owner" = [ + "group:three@example.org" + ], + } +} +# tftest modules=1 resources=5 +``` + +### Shared VPC service + +```hcl +module "project" { + source = "./modules/project" + name = "project-example" + + shared_vpc_service_config = { + attach = true + host_project = "my-host-project" + service_identity_iam = { + "roles/compute.networkUser" = [ + "cloudservices", "container-engine" + ] + "roles/vpcaccess.user" = [ + "cloudrun" + ] + "roles/container.hostServiceAgentUser" = [ + "container-engine" + ] + } + } +} +# tftest modules=1 resources=6 +``` + +### Organization policies + +```hcl +module "project" { + source = "./modules/project" + billing_account = "123456-123456-123456" + name = "project-example" + parent = "folders/1234567890" + prefix = "foo" + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + policy_boolean = { + "constraints/compute.disableGuestAttributesAccess" = true + "constraints/compute.skipDefaultNetworkCreation" = true + } + policy_list = { + "constraints/compute.trustedImageProjects" = { + inherit_from_parent = null + suggested_value = null + status = true + values = ["projects/my-project"] + } + } +} +# tftest modules=1 resources=6 +``` + +## Logging Sinks + +```hcl +module "gcs" { + source = "./modules/gcs" + project_id = var.project_id + name = "gcs_sink" + force_destroy = true +} + +module "dataset" { + source = "./modules/bigquery-dataset" + project_id = var.project_id + id = "bq_sink" +} + +module "pubsub" { + source = "./modules/pubsub" + project_id = var.project_id + name = "pubsub_sink" +} + +module "bucket" { + source = "./modules/logging-bucket" + parent_type = "project" + parent = "my-project" + id = "bucket" +} + +module "project-host" { + source = "./modules/project" + name = "my-project" + billing_account = "123456-123456-123456" + parent = "folders/1234567890" + logging_sinks = { + warnings = { + type = "storage" + destination = module.gcs.name + filter = "severity=WARNING" + iam = false + unique_writer = false + exclusions = {} + } + info = { + type = "bigquery" + destination = module.dataset.id + filter = "severity=INFO" + iam = false + unique_writer = false + exclusions = {} + } + notice = { + type = "pubsub" + destination = module.pubsub.id + filter = "severity=NOTICE" + iam = true + unique_writer = false + exclusions = {} + } + debug = { + type = "logging" + destination = module.bucket.id + filter = "severity=DEBUG" + iam = true + unique_writer = false + exclusions = { + no-compute = "logName:compute" + } + } + } + logging_exclusions = { + no-gce-instances = "resource.type=gce_instance" + } +} +# tftest modules=5 resources=12 +``` + +## Cloud KMS encryption keys + +```hcl +module "project" { + source = "./modules/project" + name = "my-project" + billing_account = "123456-123456-123456" + prefix = "foo" + services = [ + "compute.googleapis.com", + "storage.googleapis.com" + ] + service_encryption_key_ids = { + compute = [ + "projects/kms-central-prj/locations/europe-west3/keyRings/my-keyring/cryptoKeys/europe3-gce", + "projects/kms-central-prj/locations/europe-west4/keyRings/my-keyring/cryptoKeys/europe4-gce" + ] + storage = [ + "projects/kms-central-prj/locations/europe/keyRings/my-keyring/cryptoKeys/europe-gcs" + ] + } +} +# tftest modules=1 resources=7 +``` + +## 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. + +```hcl +module "org" { + source = "./modules/organization" + organization_id = var.organization_id + tags = { + environment = { + description = "Environment specification." + iam = null + values = { + dev = null + prod = null + } + } + } +} + +module "project" { + source = "./modules/project" + name = "test-project" + tag_bindings = { + env-prod = module.org.tag_values["environment/prod"].id + foo = "tagValues/12345678" + } +} +# tftest modules=2 resources=6 +``` + + + + +## Files + +| name | description | resources | +|---|---|---| +| [iam.tf](./iam.tf) | Generic and OSLogin-specific IAM bindings and roles. | google_project_iam_binding · google_project_iam_custom_role · google_project_iam_member | +| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_project_exclusion · google_logging_project_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | +| [main.tf](./main.tf) | Module-level locals and resources. | google_compute_project_metadata_item · google_essential_contacts_contact · google_monitoring_monitored_project · google_project · google_project_service · google_resource_manager_lien | +| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_project_organization_policy | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [service-accounts.tf](./service-accounts.tf) | Service identities and supporting resources. | google_kms_crypto_key_iam_member · google_project_service_identity | +| [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project · google_compute_shared_vpc_service_project · google_project_iam_member | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding | +| [variables.tf](./variables.tf) | Module variables. | | +| [versions.tf](./versions.tf) | Version pins. | | +| [vpc-sc.tf](./vpc-sc.tf) | VPC-SC project-level perimeter configuration. | google_access_context_manager_service_perimeter_resource | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [name](variables.tf#L125) | Project name and id suffix. | string | ✓ | | +| [auto_create_network](variables.tf#L17) | Whether to create the default network for the project. | bool | | false | +| [billing_account](variables.tf#L23) | Billing account id. | string | | null | +| [contacts](variables.tf#L29) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | +| [custom_roles](variables.tf#L36) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| [descriptive_name](variables.tf#L43) | Name of the project name. Used for project name instead of `name` variable. | string | | null | +| [group_iam](variables.tf#L49) | 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. | map(list(string)) | | {} | +| [iam](variables.tf#L56) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive](variables.tf#L63) | IAM additive bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive_members](variables.tf#L70) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | map(list(string)) | | {} | +| [labels](variables.tf#L76) | Resource labels. | map(string) | | {} | +| [lien_reason](variables.tf#L83) | If non-empty, creates a project lien with this description. | string | | "" | +| [logging_exclusions](variables.tf#L89) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L96) | Logging sinks to create for this project. | map(object({…})) | | {} | +| [metric_scopes](variables.tf#L118) | List of projects that will act as metric scopes for this project. | list(string) | | [] | +| [oslogin](variables.tf#L130) | Enable OS Login. | bool | | false | +| [oslogin_admins](variables.tf#L136) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | +| [oslogin_users](variables.tf#L144) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | +| [parent](variables.tf#L151) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [policy_boolean](variables.tf#L161) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | +| [policy_list](variables.tf#L168) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | +| [prefix](variables.tf#L180) | Prefix used to generate project id and name. | string | | null | +| [project_create](variables.tf#L186) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | +| [service_config](variables.tf#L192) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L204) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | +| [service_perimeter_bridges](variables.tf#L211) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | +| [service_perimeter_standard](variables.tf#L218) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | +| [services](variables.tf#L224) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L230) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L239) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | +| [skip_delete](variables.tf#L249) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [tag_bindings](variables.tf#L255) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [custom_roles](outputs.tf#L17) | Ids of the created custom roles. | | +| [name](outputs.tf#L25) | Project name. | | +| [number](outputs.tf#L38) | Project number. | | +| [project_id](outputs.tf#L51) | Project id. | | +| [service_accounts](outputs.tf#L66) | Product robot service accounts in project. | | +| [sink_writer_identities](outputs.tf#L82) | Writer identities created for each sink. | | + + diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project/iam.tf b/examples/guardrails/terraform-cloud/project-factory/modules/project/iam.tf new file mode 100644 index 0000000..69925cc --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project/iam.tf @@ -0,0 +1,115 @@ +/** + * 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 + * + * 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 Generic and OSLogin-specific IAM bindings and roles. + +# IAM notes: +# - external users need to have accepted the invitation email to join +# - oslogin roles also require role to list instances +# - additive (non-authoritative) roles might fail due to dynamic values + +locals { + _group_iam_roles = distinct(flatten(values(var.group_iam))) + _group_iam = { + for r in local._group_iam_roles : r => [ + 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( + try(var.iam[role], []), + 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_project_iam_custom_role" "roles" { + for_each = var.custom_roles + project = local.project.project_id + role_id = each.key + title = "Custom role ${each.key}" + description = "Terraform-managed." + permissions = each.value +} + +resource "google_project_iam_binding" "authoritative" { + for_each = local.iam + project = local.project.project_id + role = each.key + members = each.value + depends_on = [ + google_project_service.project_services, + google_project_iam_custom_role.roles + ] +} + +resource "google_project_iam_member" "additive" { + for_each = ( + length(var.iam_additive) + length(var.iam_additive_members) > 0 + ? local.iam_additive + : {} + ) + project = local.project.project_id + role = each.value.role + member = each.value.member + depends_on = [ + google_project_service.project_services, + google_project_iam_custom_role.roles + ] +} + +resource "google_project_iam_member" "oslogin_iam_serviceaccountuser" { + for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([]) + project = local.project.project_id + role = "roles/iam.serviceAccountUser" + member = each.value +} + +resource "google_project_iam_member" "oslogin_compute_viewer" { + for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([]) + project = local.project.project_id + role = "roles/compute.viewer" + member = each.value +} + +resource "google_project_iam_member" "oslogin_admins" { + for_each = var.oslogin ? toset(var.oslogin_admins) : toset([]) + project = local.project.project_id + role = "roles/compute.osAdminLogin" + member = each.value +} + +resource "google_project_iam_member" "oslogin_users" { + for_each = var.oslogin ? toset(var.oslogin_users) : toset([]) + project = local.project.project_id + role = "roles/compute.osLogin" + member = each.value +} diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project/logging.tf b/examples/guardrails/terraform-cloud/project-factory/modules/project/logging.tf new file mode 100644 index 0000000..04d7abf --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project/logging.tf @@ -0,0 +1,91 @@ +/** + * 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 + * + * 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 Log sinks and supporting resources. + +locals { + sink_bindings = { + for type in ["bigquery", "pubsub", "logging", "storage"] : + type => { + for name, sink in var.logging_sinks : + name => sink if sink.iam && sink.type == type + } + } +} + +resource "google_logging_project_sink" "sink" { + for_each = var.logging_sinks + name = each.key + #description = "${each.key} (Terraform-managed)." + project = local.project.project_id + destination = "${each.value.type}.googleapis.com/${each.value.destination}" + filter = each.value.filter + unique_writer_identity = each.value.unique_writer + + dynamic "exclusions" { + for_each = each.value.exclusions + iterator = exclusion + content { + name = exclusion.key + filter = exclusion.value + } + } + + depends_on = [ + google_project_iam_binding.authoritative, + google_project_iam_member.additive + ] +} + +resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { + for_each = local.sink_bindings["storage"] + bucket = each.value.destination + role = "roles/storage.objectCreator" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" { + for_each = local.sink_bindings["bigquery"] + project = split("/", each.value.destination)[1] + dataset_id = split("/", each.value.destination)[3] + role = "roles/bigquery.dataEditor" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" { + for_each = local.sink_bindings["pubsub"] + project = split("/", each.value.destination)[1] + topic = split("/", each.value.destination)[3] + role = "roles/pubsub.publisher" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_project_iam_member" "bucket-sinks-binding" { + for_each = local.sink_bindings["logging"] + project = split("/", each.value.destination)[1] + role = "roles/logging.bucketWriter" + member = google_logging_project_sink.sink[each.key].writer_identity + # TODO(jccb): use a condition to limit writer-identity only to this + # bucket +} + +resource "google_logging_project_exclusion" "logging-exclusion" { + for_each = var.logging_exclusions + name = each.key + project = local.project.project_id + description = "${each.key} (Terraform-managed)." + filter = each.value +} diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project/main.tf b/examples/guardrails/terraform-cloud/project-factory/modules/project/main.tf new file mode 100644 index 0000000..9f0dff4 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project/main.tf @@ -0,0 +1,95 @@ +/** + * 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 + * + * 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 { + descriptive_name = ( + var.descriptive_name != null ? var.descriptive_name : "${local.prefix}${var.name}" + ) + parent_type = var.parent == null ? null : split("/", var.parent)[0] + parent_id = var.parent == null ? null : split("/", var.parent)[1] + prefix = var.prefix == null ? "" : "${var.prefix}-" + project = ( + var.project_create ? + { + project_id = try(google_project.project.0.project_id, null) + number = try(google_project.project.0.number, null) + name = try(google_project.project.0.name, null) + } + : { + project_id = "${local.prefix}${var.name}" + number = try(data.google_project.project.0.number, null) + name = try(data.google_project.project.0.name, null) + } + ) +} + +data "google_project" "project" { + count = var.project_create ? 0 : 1 + project_id = "${local.prefix}${var.name}" +} + +resource "google_project" "project" { + count = var.project_create ? 1 : 0 + org_id = local.parent_type == "organizations" ? local.parent_id : null + folder_id = local.parent_type == "folders" ? local.parent_id : null + project_id = "${local.prefix}${var.name}" + name = local.descriptive_name + billing_account = var.billing_account + auto_create_network = var.auto_create_network + labels = var.labels + skip_delete = var.skip_delete +} + +resource "google_project_service" "project_services" { + for_each = toset(var.services) + project = local.project.project_id + service = each.value + disable_on_destroy = var.service_config.disable_on_destroy + disable_dependent_services = var.service_config.disable_dependent_services +} + +resource "google_compute_project_metadata_item" "oslogin_meta" { + count = var.oslogin ? 1 : 0 + project = local.project.project_id + key = "enable-oslogin" + value = "TRUE" + # depend on services or it will fail on destroy + depends_on = [google_project_service.project_services] +} + +resource "google_resource_manager_lien" "lien" { + count = var.lien_reason != "" ? 1 : 0 + parent = "projects/${local.project.number}" + restrictions = ["resourcemanager.projects.delete"] + origin = "created-by-terraform" + reason = var.lien_reason +} + +resource "google_essential_contacts_contact" "contact" { + provider = google-beta + for_each = var.contacts + parent = "projects/${local.project.project_id}" + email = each.key + language_tag = "en" + notification_category_subscriptions = each.value +} + +resource "google_monitoring_monitored_project" "primary" { + provider = google-beta + for_each = toset(var.metric_scopes) + metrics_scope = each.value + name = local.project.project_id +} diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project/organization-policies.tf b/examples/guardrails/terraform-cloud/project-factory/modules/project/organization-policies.tf new file mode 100644 index 0000000..6870754 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project/organization-policies.tf @@ -0,0 +1,90 @@ +/** + * 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 + * + * 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 Project-level organization policies. + +resource "google_project_organization_policy" "boolean" { + for_each = var.policy_boolean + project = local.project.project_id + constraint = each.key + + dynamic "boolean_policy" { + for_each = each.value == null ? [] : [each.value] + iterator = policy + content { + enforced = policy.value + } + } + + dynamic "restore_policy" { + for_each = each.value == null ? [""] : [] + content { + default = true + } + } +} + +resource "google_project_organization_policy" "list" { + for_each = var.policy_list + project = local.project.project_id + constraint = each.key + + dynamic "list_policy" { + for_each = each.value.status == null ? [] : [each.value] + iterator = policy + content { + inherit_from_parent = policy.value.inherit_from_parent + suggested_value = policy.value.suggested_value + dynamic "allow" { + for_each = policy.value.status ? [""] : [] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + dynamic "deny" { + for_each = policy.value.status ? [] : [""] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + } + } + + dynamic "restore_policy" { + for_each = each.value.status == null ? [true] : [] + content { + default = true + } + } +} diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project/outputs.tf b/examples/guardrails/terraform-cloud/project-factory/modules/project/outputs.tf new file mode 100644 index 0000000..10d0e55 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project/outputs.tf @@ -0,0 +1,87 @@ +/** + * 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 + * + * 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. + */ + +output "custom_roles" { + description = "Ids of the created custom roles." + value = { + for name, role in google_project_iam_custom_role.roles : + name => role.id + } +} + +output "name" { + description = "Project name." + value = local.project.name + depends_on = [ + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "number" { + description = "Project number." + value = local.project.number + depends_on = [ + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "project_id" { + description = "Project id." + value = "${local.prefix}${var.name}" + depends_on = [ + google_project.project, + data.google_project.project, + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "service_accounts" { + description = "Product robot service accounts in project." + value = { + cloud_services = local.service_account_cloud_services + default = local.service_accounts_default + robots = local.service_accounts_robots + } + depends_on = [ + google_project_service.project_services, + google_kms_crypto_key_iam_member.service_identity_cmek, + google_project_service_identity.jit_si, + data.google_bigquery_default_service_account.bq_sa, + data.google_storage_project_service_account.gcs_sa + ] +} + +output "sink_writer_identities" { + description = "Writer identities created for each sink." + value = { + for name, sink in google_logging_project_sink.sink : name => sink.writer_identity + } +} diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project/service-accounts.tf b/examples/guardrails/terraform-cloud/project-factory/modules/project/service-accounts.tf new file mode 100644 index 0000000..3423524 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project/service-accounts.tf @@ -0,0 +1,113 @@ +/** + * 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 + * + * 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 Service identities and supporting resources. + +locals { + _service_accounts_cmek_service_dependencies = { + "composer" : [ + "composer", + "artifactregistry", "container-engine", "compute", "pubsub", "storage" + ] + "dataflow" : ["dataflow", "compute"] + } + _service_accounts_robot_services = { + artifactregistry = "service-%s@gcp-sa-artifactregistry" + bq = "bq-%s@bigquery-encryption" + cloudasset = "service-%s@gcp-sa-cloudasset" + cloudbuild = "service-%s@gcp-sa-cloudbuild" + cloudfunctions = "service-%s@gcf-admin-robot" + cloudrun = "service-%s@serverless-robot-prod" + composer = "service-%s@cloudcomposer-accounts" + compute = "service-%s@compute-system" + container-engine = "service-%s@container-engine-robot" + containerregistry = "service-%s@containerregistry" + dataflow = "service-%s@dataflow-service-producer-prod" + dataproc = "service-%s@dataproc-accounts" + gae-flex = "service-%s@gae-api-prod" + # TODO: deprecate gcf + gcf = "service-%s@gcf-admin-robot" + pubsub = "service-%s@gcp-sa-pubsub" + secretmanager = "service-%s@gcp-sa-secretmanager" + storage = "service-%s@gs-project-accounts" + } + service_accounts_default = { + compute = "${local.project.number}-compute@developer.gserviceaccount.com" + gae = "${local.project.project_id}@appspot.gserviceaccount.com" + } + service_account_cloud_services = ( + "${local.project.number}@cloudservices.gserviceaccount.com" + ) + service_accounts_robots = { + for k, v in local._service_accounts_robot_services : + k => "${format(v, local.project.number)}.iam.gserviceaccount.com" + } + service_accounts_jit_services = [ + "secretmanager.googleapis.com", + "pubsub.googleapis.com", + "cloudasset.googleapis.com" + ] + service_accounts_cmek_service_keys = distinct(flatten([ + for s in keys(var.service_encryption_key_ids) : [ + for ss in try(local._service_accounts_cmek_service_dependencies[s], [s]) : [ + for key in var.service_encryption_key_ids[s] : { + service = ss + key = key + } if key != null + ] + ] + ])) +} + +data "google_storage_project_service_account" "gcs_sa" { + count = contains(var.services, "storage.googleapis.com") ? 1 : 0 + project = local.project.project_id + depends_on = [google_project_service.project_services] +} + +data "google_bigquery_default_service_account" "bq_sa" { + count = contains(var.services, "bigquery.googleapis.com") ? 1 : 0 + project = local.project.project_id + depends_on = [google_project_service.project_services] +} + +# Secret Manager SA created just in time, we need to trigger the creation. +resource "google_project_service_identity" "jit_si" { + for_each = setintersection(var.services, local.service_accounts_jit_services) + provider = google-beta + project = local.project.project_id + service = each.value + depends_on = [google_project_service.project_services] +} + +resource "google_kms_crypto_key_iam_member" "service_identity_cmek" { + for_each = { + for service_key in local.service_accounts_cmek_service_keys : + "${service_key.service}.${service_key.key}" => service_key + if service_key != service_key.key + } + crypto_key_id = each.value.key + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + member = "serviceAccount:${local.service_accounts_robots[each.value.service]}" + depends_on = [ + google_project.project, + google_project_service.project_services, + google_project_service_identity.jit_si, + data.google_bigquery_default_service_account.bq_sa, + data.google_project.project, + data.google_storage_project_service_account.gcs_sa, + ] +} diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project/shared-vpc.tf b/examples/guardrails/terraform-cloud/project-factory/modules/project/shared-vpc.tf new file mode 100644 index 0000000..9c7bd71 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project/shared-vpc.tf @@ -0,0 +1,75 @@ +/** + * 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 + * + * 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 Shared VPC project-level configuration. + +locals { + # compute the host project IAM bindings for this project's service identities + _svpc_service_identity_iam = coalesce( + local.svpc_service_config.service_identity_iam, {} + ) + _svpc_service_iam = flatten([ + for role, services in local._svpc_service_identity_iam : [ + for service in services : { role = role, service = service } + ] + ]) + svpc_host_config = { + enabled = coalesce( + try(var.shared_vpc_host_config.enabled, null), false + ) + service_projects = coalesce( + try(var.shared_vpc_host_config.service_projects, null), [] + ) + } + svpc_service_config = coalesce(var.shared_vpc_service_config, { + host_project = null, service_identity_iam = {} + }) + svpc_service_iam = { + for b in local._svpc_service_iam : "${b.role}:${b.service}" => b + } +} + +resource "google_compute_shared_vpc_host_project" "shared_vpc_host" { + provider = google-beta + count = local.svpc_host_config.enabled ? 1 : 0 + project = local.project.project_id +} + +resource "google_compute_shared_vpc_service_project" "service_projects" { + provider = google-beta + for_each = toset(local.svpc_host_config.service_projects) + host_project = local.project.project_id + service_project = each.value + depends_on = [google_compute_shared_vpc_host_project.shared_vpc_host] +} + +resource "google_compute_shared_vpc_service_project" "shared_vpc_service" { + provider = google-beta + count = local.svpc_service_config.host_project != null ? 1 : 0 + host_project = var.shared_vpc_service_config.host_project + service_project = local.project.project_id +} + +resource "google_project_iam_member" "shared_vpc_host_robots" { + for_each = local.svpc_service_iam + project = var.shared_vpc_service_config.host_project + role = each.value.role + member = ( + each.value.service == "cloudservices" + ? "serviceAccount:${local.service_account_cloud_services}" + : "serviceAccount:${local.service_accounts_robots[each.value.service]}" + ) +} diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project/tags.tf b/examples/guardrails/terraform-cloud/project-factory/modules/project/tags.tf new file mode 100644 index 0000000..683143b --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project/tags.tf @@ -0,0 +1,21 @@ +/** + * 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 + * + * 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. + */ + +resource "google_tags_tag_binding" "binding" { + for_each = coalesce(var.tag_bindings, {}) + parent = "//cloudresourcemanager.googleapis.com/projects/${local.project.number}" + tag_value = each.value +} diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project/variables.tf b/examples/guardrails/terraform-cloud/project-factory/modules/project/variables.tf new file mode 100644 index 0000000..578f9d2 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project/variables.tf @@ -0,0 +1,259 @@ +/** + * 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 + * + * 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. + */ + +variable "auto_create_network" { + description = "Whether to create the default network for the project." + type = bool + default = false +} + +variable "billing_account" { + description = "Billing account id." + type = string + default = null +} + +variable "contacts" { + description = "List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES." + type = map(list(string)) + default = {} + nullable = false +} + +variable "custom_roles" { + description = "Map of role name => list of permissions to create in this project." + type = map(list(string)) + default = {} + nullable = false +} + +variable "descriptive_name" { + description = "Name of the project name. Used for project name instead of `name` variable." + type = string + default = null +} + +variable "group_iam" { + description = "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." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam" { + description = "IAM bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_additive" { + description = "IAM additive bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +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 "labels" { + description = "Resource labels." + type = map(string) + default = {} + nullable = false +} + +variable "lien_reason" { + description = "If non-empty, creates a project lien with this description." + type = string + default = "" +} + +variable "logging_exclusions" { + description = "Logging exclusions for this project in the form {NAME -> FILTER}." + type = map(string) + default = {} + nullable = false +} + +variable "logging_sinks" { + description = "Logging sinks to create for this project." + type = map(object({ + destination = string + type = string + filter = string + iam = bool + unique_writer = bool + # TODO exclusions also support description and disabled + exclusions = map(string) + })) + validation { + condition = alltrue([ + for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } + default = {} + nullable = false +} + +variable "metric_scopes" { + description = "List of projects that will act as metric scopes for this project." + type = list(string) + default = [] + nullable = false +} + +variable "name" { + description = "Project name and id suffix." + type = string +} + +variable "oslogin" { + description = "Enable OS Login." + type = bool + default = false +} + +variable "oslogin_admins" { + description = "List of IAM-style identities that will be granted roles necessary for OS Login administrators." + type = list(string) + default = [] + nullable = false + +} + +variable "oslogin_users" { + description = "List of IAM-style identities that will be granted roles necessary for OS Login users." + type = list(string) + default = [] + nullable = false +} + +variable "parent" { + description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "policy_boolean" { + description = "Map of boolean org policies and enforcement value, set value to null for policy restore." + type = map(bool) + default = {} + nullable = false +} + +variable "policy_list" { + description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." + type = map(object({ + inherit_from_parent = bool + suggested_value = string + status = bool + values = list(string) + })) + default = {} + nullable = false +} + +variable "prefix" { + description = "Prefix used to generate project id and name." + type = string + default = null +} + +variable "project_create" { + description = "Create project. When set to false, uses a data source to reference existing project." + type = bool + default = true +} + +variable "service_config" { + description = "Configure service API activation." + type = object({ + disable_on_destroy = bool + disable_dependent_services = bool + }) + default = { + disable_on_destroy = true + disable_dependent_services = true + } +} + +variable "service_encryption_key_ids" { + description = "Cloud KMS encryption key in {SERVICE => [KEY_URL]} format." + type = map(list(string)) + default = {} +} + +# accessPolicies/ACCESS_POLICY_NAME/servicePerimeters/PERIMETER_NAME +variable "service_perimeter_bridges" { + description = "Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format." + type = list(string) + default = null +} + +# accessPolicies/ACCESS_POLICY_NAME/servicePerimeters/PERIMETER_NAME +variable "service_perimeter_standard" { + description = "Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format." + type = string + default = null +} + +variable "services" { + description = "Service APIs to enable." + type = list(string) + default = [] +} + +variable "shared_vpc_host_config" { + description = "Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project)." + type = object({ + enabled = bool + service_projects = list(string) + }) + default = null +} + +variable "shared_vpc_service_config" { + description = "Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config)." + # the list of valid service identities is in service-accounts.tf + type = object({ + host_project = string + service_identity_iam = map(list(string)) + }) + default = null +} + +variable "skip_delete" { + description = "Allows the underlying resources to be destroyed without destroying the project itself." + type = bool + default = false +} + +variable "tag_bindings" { + description = "Tag bindings for this project, in key => tag value id format." + type = map(string) + default = null +} diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project/versions.tf b/examples/guardrails/terraform-cloud/project-factory/modules/project/versions.tf new file mode 100644 index 0000000..e72a780 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.1.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project/vpc-sc.tf b/examples/guardrails/terraform-cloud/project-factory/modules/project/vpc-sc.tf new file mode 100644 index 0000000..edaa203 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project/vpc-sc.tf @@ -0,0 +1,45 @@ +/** + * 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 + * + * 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 VPC-SC project-level perimeter configuration. + +moved { + from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-standard + to = google_access_context_manager_service_perimeter_resource.standard +} + +resource "google_access_context_manager_service_perimeter_resource" "standard" { + count = var.service_perimeter_standard != null ? 1 : 0 + # this needs an additional lifecycle block in the vpc module on the + # google_access_context_manager_service_perimeter resource + perimeter_name = var.service_perimeter_standard + resource = "projects/${local.project.number}" +} + +moved { + from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-bridges + to = google_access_context_manager_service_perimeter_resource.bridge +} + +resource "google_access_context_manager_service_perimeter_resource" "bridge" { + for_each = toset( + var.service_perimeter_bridges != null ? var.service_perimeter_bridges : [] + ) + # this needs an additional lifecycle block in the vpc module on the + # google_access_context_manager_service_perimeter resource + perimeter_name = each.value + resource = "projects/${local.project.number}" +} diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project_plus/README.md b/examples/guardrails/terraform-cloud/project-factory/modules/project_plus/README.md new file mode 100644 index 0000000..be0deaf --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project_plus/README.md @@ -0,0 +1 @@ +This is an addon for the project module with connects one service account to one project and tie that Service account to IAM Role . IAM Role will have the Workspace ID on Terraform Cloud, which will ensure that only that specific workspace is allowed to be deploy to GCP. \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project_plus/main.tf b/examples/guardrails/terraform-cloud/project-factory/modules/project_plus/main.tf new file mode 100644 index 0000000..b5f32a5 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project_plus/main.tf @@ -0,0 +1,47 @@ +/** + * 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 + * + * 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. + */ + +resource "random_id" "rand" { + byte_length = 4 +} + +module "project" { + source = "./../project" + name = "${var.team}-prj-${random_id.rand.hex}" + parent = var.folder + billing_account = var.billing_account +} + +resource "google_service_account" "sa" { + account_id = "${var.team}-sa-${random_id.rand.hex}" + display_name = "Service account ${var.team}" + project = module.project.project_id +} + +resource "google_service_account_iam_member" "sa-iam" { + service_account_id = google_service_account.sa.name + role = "roles/iam.workloadIdentityUser" + #member = "principalSet://iam.googleapis.com/${var.wif-pool}/attribute.sub/${var.repo_sub}" + member = "principalSet://iam.googleapis.com/${var.wif-pool}/attribute.terraform_workspace_id/${var.tfe_workspace_id}" +} + +resource "google_project_iam_member" "sa-project" { + for_each = toset(var.roles) + role = each.value + member = "serviceAccount:${google_service_account.sa.email}" + project = module.project.project_id + depends_on = [google_service_account.sa] +} diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project_plus/outputs.tf b/examples/guardrails/terraform-cloud/project-factory/modules/project_plus/outputs.tf new file mode 100644 index 0000000..c7e5321 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project_plus/outputs.tf @@ -0,0 +1,36 @@ +/** + * 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 + * + * 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. + */ + +output "project_id" { + description = "Project ID" + value = module.project.project_id +} + +output "service_account_email" { + description = "Service Account Email" + value = google_service_account.sa.email +} + +# output "repo_sub" { +# description = "Repository" +# value = var.repo_sub +# } + +# output "repo_provider" { +# description = "Repository Provider" +# value = var.repo_provider +# } + diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project_plus/variables.tf b/examples/guardrails/terraform-cloud/project-factory/modules/project_plus/variables.tf new file mode 100644 index 0000000..bba5b24 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project_plus/variables.tf @@ -0,0 +1,57 @@ +/** + * 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 + * + * 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. + */ + + +variable "team" { + description = "Team name." + type = string +} + +# variable "repo_sub" { +# description = "Repository path" +# type = string +# } + +# variable "repo_provider" { +# description = "Repository provider" +# type = string +# } + +variable "billing_account" { + description = "Billing account name." + type = string +} + +variable "folder" { + description = "Folder name." + type = string +} + +variable "roles" { + description = "Roles to attach." + type = list(string) + default = [] +} + +variable "wif-pool" { + description = "WIF pool name." + type = string +} + +variable "tfe_workspace_id" { + description = "TFE workspace id." + type = string +} \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/project-factory/modules/project_plus/versions.tf b/examples/guardrails/terraform-cloud/project-factory/modules/project_plus/versions.tf new file mode 100644 index 0000000..2904126 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/modules/project_plus/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.0.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/terraform-cloud/project-factory/outputs.tf b/examples/guardrails/terraform-cloud/project-factory/outputs.tf new file mode 100644 index 0000000..fe9a139 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/outputs.tf @@ -0,0 +1,45 @@ +/** + * 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 + * + * 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. + */ + +output "wif_pool_id_gitlab" { + description = "Gitlab Workload Identity Pool" + value = google_iam_workload_identity_pool.wif-pool-gitlab.name +} + +output "wif_provider_id_gitlab" { + description = "Gitlab Workload Identity Pool" + value = google_iam_workload_identity_pool_provider.wif-provider-gitlab.name +} + +# output "wif_pool_id_github" { +# description = "Github Workload Identity Pool" +# value = google_iam_workload_identity_pool.wif-pool-github.name +# } + +# output "wif_provider_id_github" { +# description = "Github Workload Identity Pool" +# value = google_iam_workload_identity_pool_provider.wif-provider-github.name +# } + +output "projects" { + description = "Created projects and service accounts." + value = module.project +} + +output "workload_identity_audience" { + description = "TFC Workload Identity Audience." + value = "//iam.googleapis.com/${google_iam_workload_identity_pool_provider.wif-provider-gitlab.name}" +} \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/project-factory/provider.tf b/examples/guardrails/terraform-cloud/project-factory/provider.tf new file mode 100644 index 0000000..0311d4b --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/provider.tf @@ -0,0 +1,44 @@ +/** + * 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 + * + * 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. + */ + +# terraform { +# backend "gcs" { +# } +# } + +# provider "google" { + +# } + +# provider "google-beta" { + +# } + + +module "tfe_oidc" { + source = "./tfc-oidc" + + impersonate_service_account_email = var.impersonate_service_account_email +} + +provider "google" { + credentials = module.tfe_oidc.credentials +} + + +provider "google-beta" { + credentials = module.tfe_oidc.credentials +} \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/project-factory/tfc-oidc/README.md b/examples/guardrails/terraform-cloud/project-factory/tfc-oidc/README.md new file mode 100644 index 0000000..534d659 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/tfc-oidc/README.md @@ -0,0 +1,38 @@ +# Terraform Enterprise OIDC Credential for GCP Workload Identity Federation + +This is a helper module to prepare GCP Credentials from Terraform Enterprise workload identity token. For more information see [Terraform Enterprise Workload Identity Federation](../) blueprint. + +## Example +```hcl +module "tfe_oidc" { + source = "./tfc-oidc" + + impersonate_service_account_email = "tfe-test@tfe-test-wif.iam.gserviceaccount.com" +} + +provider "google" { + credentials = module.tfe_oidc.credentials +} + +provider "google-beta" { + credentials = module.tfe_oidc.credentials +} + +# tftest skip +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [impersonate_service_account_email](variables.tf#L17) | Service account to be impersonated by workload identity federation. | string | ✓ | | +| [tmp_oidc_token_path](variables.tf#L22) | Name of the temporary file where TFC OIDC token will be stored to authentificate terraform provider google. | string | | ".oidc_token" | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [credentials](outputs.tf#L17) | Credentials in format to pass the to gcp provider. | | + + diff --git a/examples/guardrails/terraform-cloud/project-factory/tfc-oidc/get_audience.sh b/examples/guardrails/terraform-cloud/project-factory/tfc-oidc/get_audience.sh new file mode 100644 index 0000000..251fe32 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/tfc-oidc/get_audience.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# 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. + +# Exit if any of the intermediate steps fail +set -e + +cat < $FILENAME + +echo -n "{\"file\":\"${FILENAME}\"}" diff --git a/examples/guardrails/terraform-cloud/project-factory/variables.tf b/examples/guardrails/terraform-cloud/project-factory/variables.tf new file mode 100644 index 0000000..4a22578 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/variables.tf @@ -0,0 +1,26 @@ +/** + * 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 + * + * 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. + */ + +variable "folder" { + default = "folders/714215143280" +} + + +variable "impersonate_service_account_email" { + description = "Service account to be impersonated by workload identity." + type = string + #default = "test-sa@iac-prd-v2.iam.gserviceaccount.com" +} \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/project-factory/wif.tf b/examples/guardrails/terraform-cloud/project-factory/wif.tf new file mode 100644 index 0000000..aa7b416 --- /dev/null +++ b/examples/guardrails/terraform-cloud/project-factory/wif.tf @@ -0,0 +1,83 @@ +/** + * 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 + * + * 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. + */ + +resource "random_id" "rand" { + byte_length = 4 +} + +module "wif-project" { + source = "./modules/project" + name = "wif1-prj-${random_id.rand.hex}" + parent = var.folder + billing_account = "019609-F059A9-76DC20" + services = [ + "iam.googleapis.com", + "cloudresourcemanager.googleapis.com", + "iamcredentials.googleapis.com", + "sts.googleapis.com", + ] + } + +resource "google_iam_workload_identity_pool" "wif-pool-gitlab" { + provider = google-beta + workload_identity_pool_id = "gitlab-pool-${random_id.rand.hex}" + project = module.wif-project.project_id +} + +resource "google_iam_workload_identity_pool_provider" "wif-provider-gitlab" { + provider = google-beta + workload_identity_pool_id = google_iam_workload_identity_pool.wif-pool-gitlab.workload_identity_pool_id + workload_identity_pool_provider_id = "gitlab-provider-${random_id.rand.hex}" + project = module.wif-project.project_id + #attribute_condition = "attribute.terraform_organization_id == \"${each.value.tfe_workspace_id}\"" + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.sub" = "assertion.sub" + "attribute.aud" = "assertion.aud" + "attribute.terraform_run_phase" = "assertion.terraform_run_phase" + "attribute.terraform_workspace_id" = "assertion.terraform_workspace_id" + "attribute.terraform_workspace_name" = "assertion.terraform_workspace_name" + "attribute.terraform_organization_id" = "assertion.terraform_organization_id" + "attribute.terraform_organization_name" = "assertion.terraform_organization_name" + "attribute.terraform_run_id" = "assertion.terraform_run_id" + "attribute.terraform_full_workspace" = "assertion.terraform_full_workspace" + } + oidc { + #allowed_audiences = ["https://gitlab.com"] + issuer_uri = "https://app.terraform.io/" + } +} + +# resource "google_iam_workload_identity_pool" "wif-pool-github" { +# provider = google-beta +# workload_identity_pool_id = "github-pool-${random_id.rand.hex}" +# project = module.wif-project.project_id +# } + +# resource "google_iam_workload_identity_pool_provider" "wif-provider-github" { +# provider = google-beta +# workload_identity_pool_id = google_iam_workload_identity_pool.wif-pool-github.workload_identity_pool_id +# workload_identity_pool_provider_id = "github-provider-${random_id.rand.hex}" +# project = module.wif-project.project_id +# attribute_mapping = { +# "google.subject" = "assertion.sub" +# "attribute.sub" = "assertion.sub" +# "attribute.actor" = "assertion.actor" +# } +# oidc { +# issuer_uri = "https://token.actions.githubusercontent.com" +# } +# } diff --git a/examples/guardrails/terraform-cloud/skunkworks/.github/workflows/tf-actions-dev.yml b/examples/guardrails/terraform-cloud/skunkworks/.github/workflows/tf-actions-dev.yml new file mode 100644 index 0000000..2fbf4c1 --- /dev/null +++ b/examples/guardrails/terraform-cloud/skunkworks/.github/workflows/tf-actions-dev.yml @@ -0,0 +1,91 @@ +# 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 +# +# 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. + +name: 'DEV Deployment' + +on: + push: + branches: + - dev + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.DEV_SERVICE_ACCOUNT }} + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve + + diff --git a/examples/guardrails/terraform-cloud/skunkworks/.github/workflows/tf-actions-prod.yml b/examples/guardrails/terraform-cloud/skunkworks/.github/workflows/tf-actions-prod.yml new file mode 100644 index 0000000..4159feb --- /dev/null +++ b/examples/guardrails/terraform-cloud/skunkworks/.github/workflows/tf-actions-prod.yml @@ -0,0 +1,91 @@ +# 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 +# +# 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. + +name: 'PROD Deployment' + +on: + push: + branches: + - main + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.PROD_SERVICE_ACCOUNT }} + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve + + diff --git a/examples/guardrails/terraform-cloud/skunkworks/.github/workflows/tf-actions-stage.yml b/examples/guardrails/terraform-cloud/skunkworks/.github/workflows/tf-actions-stage.yml new file mode 100644 index 0000000..0f20fa0 --- /dev/null +++ b/examples/guardrails/terraform-cloud/skunkworks/.github/workflows/tf-actions-stage.yml @@ -0,0 +1,91 @@ +# 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 +# +# 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. + +name: 'STAGE Deployment' + +on: + push: + branches: + - stage + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.STAGE_SERVICE_ACCOUNT }} + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve + + diff --git a/examples/guardrails/terraform-cloud/skunkworks/README.md b/examples/guardrails/terraform-cloud/skunkworks/README.md new file mode 100644 index 0000000..ba78da6 --- /dev/null +++ b/examples/guardrails/terraform-cloud/skunkworks/README.md @@ -0,0 +1,37 @@ + +# Skunkworks - IaC Kickstarter Template + +This is a template for an IaC kickstarter repository. + +![Skunkworks](https://user-images.githubusercontent.com/94000358/169810982-36f01de2-e5e5-4ecd-b98e-3cf5a6aa9f81.png) + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +This template will use the project and service account created in project factory to deploy reosurces for Skunkworks. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + + +- Ensure to have a Workspace created on terraform Cloud which would have Gitlab Repository as the VCS Source + +- Outputs of Project Factory, will give the variable values for below variables, and the same has to be updated on the terraform workspace + +``` +env: + impersonate_service_account_email: 'xxx@project.iam.gserviceaccount.com' + # The Service Account used to create Folder + + project: 'xxxx' + # Project in which resource will be created + + TFC_WORKLOAD_IDENTITY_AUDIENCE: '//iam.googleapis.com/projects/id/locations/global/workloadIdentityPools//providers/' + # WorkLoad Identity Audience will be used by tfc-oidc module for token generation and impersonation +``` + + +> **_NOTE:_** You can create multiple branches to control the Skunkworks deployment workflow. You can upadte the Terraform Workspace below Settings +> - Terraform Working Directory +> - VCS Branch \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/skunkworks/main.tf b/examples/guardrails/terraform-cloud/skunkworks/main.tf new file mode 100644 index 0000000..6975ecb --- /dev/null +++ b/examples/guardrails/terraform-cloud/skunkworks/main.tf @@ -0,0 +1,22 @@ +/** + * 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 + * + * 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. + */ + +resource "google_storage_bucket" "bucket" { + project = var.project + name = lower("${var.project}-test-bucket") + location = "EU" + force_destroy = true +} diff --git a/examples/guardrails/terraform-cloud/skunkworks/provider.tf b/examples/guardrails/terraform-cloud/skunkworks/provider.tf new file mode 100644 index 0000000..ec315e7 --- /dev/null +++ b/examples/guardrails/terraform-cloud/skunkworks/provider.tf @@ -0,0 +1,30 @@ +/** + * 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 + * + * 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. + */ + +module "tfe_oidc" { + source = "./tfc-oidc" + + impersonate_service_account_email = var.impersonate_service_account_email +} + +provider "google" { + credentials = module.tfe_oidc.credentials +} + + +provider "google-beta" { + credentials = module.tfe_oidc.credentials +} \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/skunkworks/tfc-oidc/README.md b/examples/guardrails/terraform-cloud/skunkworks/tfc-oidc/README.md new file mode 100644 index 0000000..534d659 --- /dev/null +++ b/examples/guardrails/terraform-cloud/skunkworks/tfc-oidc/README.md @@ -0,0 +1,38 @@ +# Terraform Enterprise OIDC Credential for GCP Workload Identity Federation + +This is a helper module to prepare GCP Credentials from Terraform Enterprise workload identity token. For more information see [Terraform Enterprise Workload Identity Federation](../) blueprint. + +## Example +```hcl +module "tfe_oidc" { + source = "./tfc-oidc" + + impersonate_service_account_email = "tfe-test@tfe-test-wif.iam.gserviceaccount.com" +} + +provider "google" { + credentials = module.tfe_oidc.credentials +} + +provider "google-beta" { + credentials = module.tfe_oidc.credentials +} + +# tftest skip +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [impersonate_service_account_email](variables.tf#L17) | Service account to be impersonated by workload identity federation. | string | ✓ | | +| [tmp_oidc_token_path](variables.tf#L22) | Name of the temporary file where TFC OIDC token will be stored to authentificate terraform provider google. | string | | ".oidc_token" | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [credentials](outputs.tf#L17) | Credentials in format to pass the to gcp provider. | | + + diff --git a/examples/guardrails/terraform-cloud/skunkworks/tfc-oidc/get_audience.sh b/examples/guardrails/terraform-cloud/skunkworks/tfc-oidc/get_audience.sh new file mode 100644 index 0000000..251fe32 --- /dev/null +++ b/examples/guardrails/terraform-cloud/skunkworks/tfc-oidc/get_audience.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# 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. + +# Exit if any of the intermediate steps fail +set -e + +cat < $FILENAME + +echo -n "{\"file\":\"${FILENAME}\"}" diff --git a/examples/guardrails/terraform-cloud/skunkworks/variables.tf b/examples/guardrails/terraform-cloud/skunkworks/variables.tf new file mode 100644 index 0000000..19c15d8 --- /dev/null +++ b/examples/guardrails/terraform-cloud/skunkworks/variables.tf @@ -0,0 +1,26 @@ +/** + * 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 + * + * 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. + */ + +variable "project" { + type = string + default = "project-id" +} + + +variable "impersonate_service_account_email" { + description = "Service account to be impersonated by workload identity." + type = string +} From c2f665694fb6e932bae141782fcc244e89e708bb Mon Sep 17 00:00:00 2001 From: agutta Date: Wed, 1 Feb 2023 16:16:04 -0600 Subject: [PATCH 004/132] Fixing Link for Folders Fixing Link for Folders --- examples/guardrails/gitlab/folder-factory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/folder-factory/README.md b/examples/guardrails/gitlab/folder-factory/README.md index a139948..aa5036b 100644 --- a/examples/guardrails/gitlab/folder-factory/README.md +++ b/examples/guardrails/gitlab/folder-factory/README.md @@ -56,4 +56,4 @@ iam: - serviceAccount:XXXXX@XXXXXX ``` -Every folder is defined with its own yaml file located in the following [Folder](data/folder). +Every folder is defined with its own yaml file located in the following [Folder](data/folders). From d91fdc411d32d666043c5453002383001dd66b0c Mon Sep 17 00:00:00 2001 From: agutta Date: Wed, 1 Feb 2023 16:17:58 -0600 Subject: [PATCH 005/132] Fixing Link for Folders Fixing Link for Folders --- examples/guardrails/bitbucket/folder-factory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/bitbucket/folder-factory/README.md b/examples/guardrails/bitbucket/folder-factory/README.md index a139948..aa5036b 100644 --- a/examples/guardrails/bitbucket/folder-factory/README.md +++ b/examples/guardrails/bitbucket/folder-factory/README.md @@ -56,4 +56,4 @@ iam: - serviceAccount:XXXXX@XXXXXX ``` -Every folder is defined with its own yaml file located in the following [Folder](data/folder). +Every folder is defined with its own yaml file located in the following [Folder](data/folders). From 7a0f6f87fe3ff7c9a6f23ba2aa711a39d7a9002c Mon Sep 17 00:00:00 2001 From: agutta Date: Wed, 1 Feb 2023 16:18:40 -0600 Subject: [PATCH 006/132] Fixing Link for Folders Fixing Link for Folders --- examples/guardrails/cloudbuild/folder-factory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/cloudbuild/folder-factory/README.md b/examples/guardrails/cloudbuild/folder-factory/README.md index a139948..aa5036b 100644 --- a/examples/guardrails/cloudbuild/folder-factory/README.md +++ b/examples/guardrails/cloudbuild/folder-factory/README.md @@ -56,4 +56,4 @@ iam: - serviceAccount:XXXXX@XXXXXX ``` -Every folder is defined with its own yaml file located in the following [Folder](data/folder). +Every folder is defined with its own yaml file located in the following [Folder](data/folders). From f3138c5f75dd81aa763681d0439999372aec8ff8 Mon Sep 17 00:00:00 2001 From: agutta Date: Wed, 1 Feb 2023 16:19:25 -0600 Subject: [PATCH 007/132] Fixing Link for Folders Fixing Link for Folders --- examples/guardrails/github/folder-factory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/github/folder-factory/README.md b/examples/guardrails/github/folder-factory/README.md index a139948..aa5036b 100644 --- a/examples/guardrails/github/folder-factory/README.md +++ b/examples/guardrails/github/folder-factory/README.md @@ -56,4 +56,4 @@ iam: - serviceAccount:XXXXX@XXXXXX ``` -Every folder is defined with its own yaml file located in the following [Folder](data/folder). +Every folder is defined with its own yaml file located in the following [Folder](data/folders). From 0141bb7556cfd2395a05ec25aba55a652fc3c206 Mon Sep 17 00:00:00 2001 From: agutta Date: Wed, 1 Feb 2023 16:19:50 -0600 Subject: [PATCH 008/132] Fixing Link for Folders Fixing Link for Folders --- examples/guardrails/jenkins/folder-factory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/jenkins/folder-factory/README.md b/examples/guardrails/jenkins/folder-factory/README.md index a139948..aa5036b 100644 --- a/examples/guardrails/jenkins/folder-factory/README.md +++ b/examples/guardrails/jenkins/folder-factory/README.md @@ -56,4 +56,4 @@ iam: - serviceAccount:XXXXX@XXXXXX ``` -Every folder is defined with its own yaml file located in the following [Folder](data/folder). +Every folder is defined with its own yaml file located in the following [Folder](data/folders). From 48bc47f49630c4db501d6cd97e7ec7e04202103f Mon Sep 17 00:00:00 2001 From: agutta Date: Wed, 1 Feb 2023 16:20:17 -0600 Subject: [PATCH 009/132] Fixing Link for Folders Fixing Link for Folders --- examples/guardrails/terraform-cloud/folder-factory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/terraform-cloud/folder-factory/README.md b/examples/guardrails/terraform-cloud/folder-factory/README.md index bd6fd96..5087f50 100644 --- a/examples/guardrails/terraform-cloud/folder-factory/README.md +++ b/examples/guardrails/terraform-cloud/folder-factory/README.md @@ -63,4 +63,4 @@ iam: - serviceAccount:XXXXX@XXXXXX ``` -Every folder is defined with its own yaml file located in the following [Folder](data/folder). +Every folder is defined with its own yaml file located in the following [Folder](data/folders). From a3ee7e4aa43b15b33dd09f2095e72cdd9e42b96b Mon Sep 17 00:00:00 2001 From: agutta Date: Thu, 9 Feb 2023 22:03:19 -0600 Subject: [PATCH 010/132] Update variables.tf --- .../guardrails/cloudbuild/project-factory/variables.tf | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/guardrails/cloudbuild/project-factory/variables.tf b/examples/guardrails/cloudbuild/project-factory/variables.tf index df6e79d..6b8a34e 100644 --- a/examples/guardrails/cloudbuild/project-factory/variables.tf +++ b/examples/guardrails/cloudbuild/project-factory/variables.tf @@ -15,5 +15,11 @@ */ variable "folder" { - + type = string + description = "Folder where projects will be created" +} + +variable "billing_account" { + type = string + description = "GCP Billing Account" } From d25883d9da53242e3b3c094ee66166f859423c53 Mon Sep 17 00:00:00 2001 From: agutta Date: Thu, 9 Feb 2023 22:07:47 -0600 Subject: [PATCH 011/132] Update wif.tf --- examples/guardrails/cloudbuild/project-factory/wif.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/cloudbuild/project-factory/wif.tf b/examples/guardrails/cloudbuild/project-factory/wif.tf index c54c67f..d772d7b 100644 --- a/examples/guardrails/cloudbuild/project-factory/wif.tf +++ b/examples/guardrails/cloudbuild/project-factory/wif.tf @@ -22,7 +22,7 @@ module "wif-project" { source = "./modules/project" name = "wif-prj-${random_id.rand.hex}" parent = var.folder - billing_account = "01B3B2-962224-4EEC67" + billing_account = var.billing_account } resource "google_iam_workload_identity_pool" "wif-pool-gitlab" { From 1c11750b494227dea3a7b752ad1f6ae02ee106ba Mon Sep 17 00:00:00 2001 From: kkram01 Date: Mon, 20 Feb 2023 14:36:53 +0530 Subject: [PATCH 012/132] Update README.md for the gitlab skunkworks flow. Update the skunkworks gitlab Readme.md to include details on the parent child pipeline workflow that the cicd configuration uses to execute terraform on different environments. --- .../gitlab/skunkworks/.gitlab/workflows/README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/README.md b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/README.md index 27903ae..b17b997 100644 --- a/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/README.md +++ b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/README.md @@ -5,10 +5,9 @@ This workflow runs terraform pipelines using Gitlab CICD. This document covers t ## Prerequisites -* A Gitlab project containing the project-factory repository +* A Gitlab project containing the skunkworks repository * Available Gitlab Runners for the project either self hosted or the SaaS Gitlab shared runners. -* A working WIF provider, pool setup with audience as gitlab.com with neccessary attributes and linked service account with required permissions. For further details, please refer to the official [Gitlab Documentation](https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/) - +* Project-factory is executed successfully ## Overview @@ -40,7 +39,7 @@ This workflow can execute terraform on different GCP projects depending on the b | TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | | ## Overview of the Pipeline stages -The complete workflow consists of 4 stages and 2 before-script jobs +The complete workflow contains a parent child pipeline with the parent containing just the trigger stage for each of the environments that passes relevant variables for that environment to the child pipeline which executes the core terraform workflow. The child pipeline [workflow](./workflow.yml) has 4 stages and 2 before-script jobs * before_script jobs : * gcp-auth : creates the wif credentials by impersonating the service account. From 9ac730e76249fbb604220ed600fbe829d4aec1af Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Tue, 28 Feb 2023 15:14:19 +0530 Subject: [PATCH 013/132] adding azuredevops --- .../workflows/terraform-deployment.yml | 91 ++++++ .../azuredevops/folder-factory/.gitignore | 3 + .../azuredevops/folder-factory/README.md | 59 ++++ .../folder-factory/azure-pipeline.yml | 124 +++++++ .../azuredevops/folder-factory/backend.tf | 7 + .../folder-factory/data/folders/app1.yaml | 32 ++ .../folder-factory/data/folders/dev.yaml | 30 ++ .../data/folders/folder.yaml.sample | 31 ++ .../azuredevops/folder-factory/main.tf | 32 ++ .../folder-factory/modules/folder/README.md | 299 +++++++++++++++++ .../modules/folder/firewall-policies.tf | 93 ++++++ .../folder-factory/modules/folder/iam.tf | 40 +++ .../folder-factory/modules/folder/logging.tf | 91 ++++++ .../folder-factory/modules/folder/main.tf | 43 +++ .../modules/folder/organization-policies.tf | 90 +++++ .../folder-factory/modules/folder/outputs.tf | 51 +++ .../folder-factory/modules/folder/tags.tf | 21 ++ .../modules/folder/variables.tf | 151 +++++++++ .../folder-factory/modules/folder/versions.tf | 29 ++ .../azuredevops/folder-factory/outputs.tf | 20 ++ .../azuredevops/folder-factory/provider.tf | 37 +++ .../azuredevops/folder-factory/variables.tf | 15 + .../workflows/terraform-deployment.yml | 94 ++++++ .../azuredevops/project-factory/.gitignore | 3 + .../azuredevops/project-factory/README.md | 75 +++++ .../project-factory/azure-pipeline.yml | 124 +++++++ .../azuredevops/project-factory/backend.tf | 7 + .../data/projects/dev-skunkworks.yaml | 28 ++ .../data/projects/prod-skunkworks.yaml | 28 ++ .../data/projects/project.yaml.sample | 41 +++ .../data/projects/stage-skunkworks.yaml | 28 ++ .../azuredevops/project-factory/main.tf | 35 ++ .../project-factory/modules/project/README.md | 308 ++++++++++++++++++ .../project-factory/modules/project/iam.tf | 115 +++++++ .../modules/project/logging.tf | 91 ++++++ .../project-factory/modules/project/main.tf | 95 ++++++ .../modules/project/organization-policies.tf | 90 +++++ .../modules/project/outputs.tf | 87 +++++ .../modules/project/service-accounts.tf | 113 +++++++ .../modules/project/shared-vpc.tf | 75 +++++ .../project-factory/modules/project/tags.tf | 21 ++ .../modules/project/variables.tf | 259 +++++++++++++++ .../modules/project/versions.tf | 29 ++ .../project-factory/modules/project/vpc-sc.tf | 45 +++ .../modules/project_plus/README.md | 1 + .../modules/project_plus/main.tf | 46 +++ .../modules/project_plus/outputs.tf | 36 ++ .../modules/project_plus/variables.tf | 52 +++ .../modules/project_plus/versions.tf | 29 ++ .../azuredevops/project-factory/outputs.tf | 40 +++ .../azuredevops/project-factory/provider.tf | 37 +++ .../azuredevops/project-factory/variables.tf | 19 ++ .../azuredevops/project-factory/wif.tf | 67 ++++ .../.github/workflows/tf-actions-dev.yml | 91 ++++++ .../.github/workflows/tf-actions-prod.yml | 91 ++++++ .../.github/workflows/tf-actions-stage.yml | 91 ++++++ .../azuredevops/skunkworks/README.md | 29 ++ .../azuredevops/skunkworks/azure-pipeline.yml | 124 +++++++ .../guardrails/azuredevops/skunkworks/main.tf | 23 ++ .../azuredevops/skunkworks/provider.tf | 36 ++ .../azuredevops/skunkworks/terraform.tfvars | 1 + .../azuredevops/skunkworks/variables.tf | 20 ++ 62 files changed, 4013 insertions(+) create mode 100644 examples/guardrails/azuredevops/folder-factory/.github/workflows/terraform-deployment.yml create mode 100644 examples/guardrails/azuredevops/folder-factory/.gitignore create mode 100644 examples/guardrails/azuredevops/folder-factory/README.md create mode 100644 examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml create mode 100644 examples/guardrails/azuredevops/folder-factory/backend.tf create mode 100644 examples/guardrails/azuredevops/folder-factory/data/folders/app1.yaml create mode 100644 examples/guardrails/azuredevops/folder-factory/data/folders/dev.yaml create mode 100644 examples/guardrails/azuredevops/folder-factory/data/folders/folder.yaml.sample create mode 100644 examples/guardrails/azuredevops/folder-factory/main.tf create mode 100644 examples/guardrails/azuredevops/folder-factory/modules/folder/README.md create mode 100644 examples/guardrails/azuredevops/folder-factory/modules/folder/firewall-policies.tf create mode 100644 examples/guardrails/azuredevops/folder-factory/modules/folder/iam.tf create mode 100644 examples/guardrails/azuredevops/folder-factory/modules/folder/logging.tf create mode 100644 examples/guardrails/azuredevops/folder-factory/modules/folder/main.tf create mode 100644 examples/guardrails/azuredevops/folder-factory/modules/folder/organization-policies.tf create mode 100644 examples/guardrails/azuredevops/folder-factory/modules/folder/outputs.tf create mode 100644 examples/guardrails/azuredevops/folder-factory/modules/folder/tags.tf create mode 100644 examples/guardrails/azuredevops/folder-factory/modules/folder/variables.tf create mode 100644 examples/guardrails/azuredevops/folder-factory/modules/folder/versions.tf create mode 100644 examples/guardrails/azuredevops/folder-factory/outputs.tf create mode 100644 examples/guardrails/azuredevops/folder-factory/provider.tf create mode 100644 examples/guardrails/azuredevops/folder-factory/variables.tf create mode 100644 examples/guardrails/azuredevops/project-factory/.github/workflows/terraform-deployment.yml create mode 100644 examples/guardrails/azuredevops/project-factory/.gitignore create mode 100644 examples/guardrails/azuredevops/project-factory/README.md create mode 100644 examples/guardrails/azuredevops/project-factory/azure-pipeline.yml create mode 100644 examples/guardrails/azuredevops/project-factory/backend.tf create mode 100644 examples/guardrails/azuredevops/project-factory/data/projects/dev-skunkworks.yaml create mode 100644 examples/guardrails/azuredevops/project-factory/data/projects/prod-skunkworks.yaml create mode 100644 examples/guardrails/azuredevops/project-factory/data/projects/project.yaml.sample create mode 100644 examples/guardrails/azuredevops/project-factory/data/projects/stage-skunkworks.yaml create mode 100644 examples/guardrails/azuredevops/project-factory/main.tf create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project/README.md create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project/iam.tf create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project/logging.tf create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project/main.tf create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project/organization-policies.tf create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project/outputs.tf create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project/service-accounts.tf create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project/shared-vpc.tf create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project/tags.tf create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project/variables.tf create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project/versions.tf create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project/vpc-sc.tf create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project_plus/README.md create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project_plus/main.tf create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project_plus/outputs.tf create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project_plus/variables.tf create mode 100644 examples/guardrails/azuredevops/project-factory/modules/project_plus/versions.tf create mode 100644 examples/guardrails/azuredevops/project-factory/outputs.tf create mode 100644 examples/guardrails/azuredevops/project-factory/provider.tf create mode 100644 examples/guardrails/azuredevops/project-factory/variables.tf create mode 100644 examples/guardrails/azuredevops/project-factory/wif.tf create mode 100644 examples/guardrails/azuredevops/skunkworks/.github/workflows/tf-actions-dev.yml create mode 100644 examples/guardrails/azuredevops/skunkworks/.github/workflows/tf-actions-prod.yml create mode 100644 examples/guardrails/azuredevops/skunkworks/.github/workflows/tf-actions-stage.yml create mode 100644 examples/guardrails/azuredevops/skunkworks/README.md create mode 100644 examples/guardrails/azuredevops/skunkworks/azure-pipeline.yml create mode 100644 examples/guardrails/azuredevops/skunkworks/main.tf create mode 100644 examples/guardrails/azuredevops/skunkworks/provider.tf create mode 100644 examples/guardrails/azuredevops/skunkworks/terraform.tfvars create mode 100644 examples/guardrails/azuredevops/skunkworks/variables.tf diff --git a/examples/guardrails/azuredevops/folder-factory/.github/workflows/terraform-deployment.yml b/examples/guardrails/azuredevops/folder-factory/.github/workflows/terraform-deployment.yml new file mode 100644 index 0000000..70f88cb --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/.github/workflows/terraform-deployment.yml @@ -0,0 +1,91 @@ +# 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 +# +# 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. + +name: 'Cloud Deployment' + +on: + push: + branches: + - main + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve + + diff --git a/examples/guardrails/azuredevops/folder-factory/.gitignore b/examples/guardrails/azuredevops/folder-factory/.gitignore new file mode 100644 index 0000000..a5d8c0e --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.terraform +.terraform.lock.hcl diff --git a/examples/guardrails/azuredevops/folder-factory/README.md b/examples/guardrails/azuredevops/folder-factory/README.md new file mode 100644 index 0000000..a139948 --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/README.md @@ -0,0 +1,59 @@ +# Folder Factory + +This is a template for a DevOps folder factory. + +It can be used with [https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory](https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory) and is intended to house the folder configurations: + +![Screenshot 2022-05-10 12 00 19 PM](https://user-images.githubusercontent.com/94000358/169809437-aaa8538e-3ffc-48b3-9028-84e4995de150.png) + +Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: + +Secret configuration + +or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: +``` +env: + STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' + # The workload identity provider that should be used for this repository. + SERVICE_ACCOUNT: 'XXXX@XXXX' + # The service account that should be used for this repository. +``` + +## Setting up folders + +The folder factory will: +- create a folders with defined organisational policies + +It uses YAML configuration files for every folder with the following sample structure: +``` +parent: folders/XXXXXXXXX +org_policies: + policy_boolean: + constraints/compute.disableGuestAttributesAccess: true + constraints/iam.disableServiceAccountCreation: false + constraints/iam.disableServiceAccountKeyCreation: false + constraints/iam.disableServiceAccountKeyUpload: false + constraints/gcp.disableCloudLogging: false + policy_list: + constraints/compute.vmExternalIpAccess: + inherit_from_parent: null + status: true + suggested_value: null + values: +iam: + roles/resourcemanager.projectCreator: + - serviceAccount:XXXXX@XXXXXX +``` + +Every folder is defined with its own yaml file located in the following [Folder](data/folder). diff --git a/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml b/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml new file mode 100644 index 0000000..814a166 --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml @@ -0,0 +1,124 @@ +name: $(Date:yyyyMMdd)$(Rev:.r) + +trigger: + - none +pool: + vmImage: ubuntu-latest +stages: + - stage: auth + displayName: "GCP WIF Auth" + jobs: + - job: governance_pipeline + timeoutInMinutes: 20 + steps: + - task: charleszipp.azure-pipelines-tasks-terraform.azure-pipelines-tasks-terraform-installer.TerraformInstaller@0 + displayName: install terraform + inputs: + terraformVersion: latest + + # AzureCLI task to retrieve an Azure token for the Service Principal and then exchanges it against a Service Account token using Workload Identity Federation + + - task: AzureCLI@2 + displayName: get access token + inputs: + azureSubscription: 'azuretogcp' + scriptType: 'bash' + scriptLocation: 'inlineScript' + inlineScript: | + SUBJECT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt" + SUBJECT_TOKEN=$(az account get-access-token --query accessToken --output tsv) + STS_TOKEN=$(curl --silent -0 -X POST https://sts.googleapis.com/v1/token \ + -H 'Content-Type: text/json; charset=utf-8' \ + -d @- < yamldecode(file("./data/folders/${f}")) + } +} + +module "folder" { + source = "./modules/folder" + for_each = local.folders + name = each.key + parent = each.value.parent + policy_boolean = try(each.value.org_policies.policy_boolean, {}) + policy_list = try(each.value.org_policies.policy_list, {}) + iam = try(each.value.iam, {}) +} \ No newline at end of file diff --git a/examples/guardrails/azuredevops/folder-factory/modules/folder/README.md b/examples/guardrails/azuredevops/folder-factory/modules/folder/README.md new file mode 100644 index 0000000..4f6898b --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/modules/folder/README.md @@ -0,0 +1,299 @@ +# Google Cloud Folder Module + +This module allows the creation and management of folders, including support for IAM bindings, organization policies, and hierarchical firewall rules. + +## Examples + +### IAM bindings + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + group_iam = { + "cloud-owners@example.org" = [ + "roles/owner", + "roles/resourcemanager.projectCreator" + ] + } + iam = { + "roles/owner" = ["user:one@example.com"] + } +} +# tftest modules=1 resources=3 +``` + +### Organization policies + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + policy_boolean = { + "constraints/compute.disableGuestAttributesAccess" = true + "constraints/compute.skipDefaultNetworkCreation" = true + } + policy_list = { + "constraints/compute.trustedImageProjects" = { + inherit_from_parent = null + suggested_value = null + status = true + values = ["projects/my-project"] + } + } +} +# tftest modules=1 resources=4 +``` + +### Firewall policy factory + +In the same way as for the [organization](../organization) module, the in-built factory allows you to define a single policy, using one file for rules, and an optional file for CIDR range substitution variables. Remember that non-absolute paths are relative to the root module (the folder where you run `terraform`). + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + firewall_policy_factory = { + cidr_file = "data/cidrs.yaml" + policy_name = null + rules_file = "data/rules.yaml" + } + firewall_policy_association = { + factory-policy = module.folder.firewall_policy_id["factory"] + } +} +# tftest skip +``` + +```yaml +# cidrs.yaml + +rfc1918: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 +``` + +```yaml +# rules.yaml + +allow-admins: + description: Access from the admin subnet to all subnets + direction: INGRESS + action: allow + priority: 1000 + ranges: + - $rfc1918 + ports: + all: [] + target_resources: null + enable_logging: false + +allow-ssh-from-iap: + description: Enable SSH from IAP + direction: INGRESS + action: allow + priority: 1002 + ranges: + - 35.235.240.0/20 + ports: + tcp: ["22"] + target_resources: null + enable_logging: false +``` + +### Logging Sinks + +```hcl +module "gcs" { + source = "./modules/gcs" + project_id = "my-project" + name = "gcs_sink" + force_destroy = true +} + +module "dataset" { + source = "./modules/bigquery-dataset" + project_id = "my-project" + id = "bq_sink" +} + +module "pubsub" { + source = "./modules/pubsub" + project_id = "my-project" + name = "pubsub_sink" +} + +module "bucket" { + source = "./modules/logging-bucket" + parent_type = "project" + parent = "my-project" + id = "bucket" +} + +module "folder-sink" { + source = "./modules/folder" + parent = "folders/657104291943" + name = "my-folder" + logging_sinks = { + warnings = { + type = "storage" + destination = module.gcs.name + filter = "severity=WARNING" + include_children = true + exclusions = {} + } + info = { + type = "bigquery" + destination = module.dataset.id + filter = "severity=INFO" + include_children = true + exclusions = {} + } + notice = { + type = "pubsub" + destination = module.pubsub.id + filter = "severity=NOTICE" + include_children = true + exclusions = {} + } + debug = { + type = "logging" + destination = module.bucket.id + filter = "severity=DEBUG" + include_children = true + exclusions = { + no-compute = "logName:compute" + } + } + } + logging_exclusions = { + no-gce-instances = "resource.type=gce_instance" + } +} +# tftest modules=5 resources=14 +``` + +### Hierarchical firewall policies + +```hcl +module "folder1" { + source = "./modules/folder" + parent = var.organization_id + name = "policy-container" + + firewall_policies = { + iap-policy = { + allow-iap-ssh = { + description = "Always allow ssh from IAP" + direction = "INGRESS" + action = "allow" + priority = 100 + ranges = ["35.235.240.0/20"] + ports = { tcp = ["22"] } + target_service_accounts = null + target_resources = null + logging = false + } + } + } + firewall_policy_association = { + iap-policy = "iap-policy" + } +} + +module "folder2" { + source = "./modules/folder" + parent = var.organization_id + name = "hf2" + firewall_policy_association = { + iap-policy = module.folder1.firewall_policy_id["iap-policy"] + } +} +# tftest modules=2 resources=6 +``` + +## 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. + +```hcl +module "org" { + source = "./modules/organization" + organization_id = var.organization_id + tags = { + environment = { + description = "Environment specification." + iam = null + values = { + dev = null + prod = null + } + } + } +} + +module "folder" { + source = "./modules/folder" + name = "Test" + parent = module.org.organization_id + tag_bindings = { + env-prod = module.org.tag_values["environment/prod"].id + foo = "tagValues/12345678" + } +} +# tftest modules=2 resources=6 +``` + + + + +## Files + +| name | description | resources | +|---|---|---| +| [firewall-policies.tf](./firewall-policies.tf) | None | google_compute_firewall_policy · google_compute_firewall_policy_association · google_compute_firewall_policy_rule | +| [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_folder_iam_binding | +| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_folder_exclusion · google_logging_folder_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | +| [main.tf](./main.tf) | Module-level locals and resources. | google_essential_contacts_contact · google_folder | +| [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_folder_organization_policy | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding | +| [variables.tf](./variables.tf) | Module variables. | | +| [versions.tf](./versions.tf) | Version pins. | | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | +| [firewall_policies](variables.tf#L24) | Hierarchical firewall policies created in this folder. | map(map(object({…}))) | | {} | +| [firewall_policy_association](variables.tf#L41) | The hierarchical firewall policy to associate to this folder. Must be either a key in the `firewall_policies` map or the id of a policy defined somewhere else. | map(string) | | {} | +| [firewall_policy_factory](variables.tf#L48) | Configuration for the firewall policy factory. | object({…}) | | null | +| [folder_create](variables.tf#L58) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | +| [group_iam](variables.tf#L64) | 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. | map(list(string)) | | {} | +| [iam](variables.tf#L71) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [id](variables.tf#L78) | Folder ID in case you use folder_create=false. | string | | null | +| [logging_exclusions](variables.tf#L84) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L91) | Logging sinks to create for this folder. | map(object({…})) | | {} | +| [name](variables.tf#L112) | Folder name. | string | | null | +| [parent](variables.tf#L118) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [policy_boolean](variables.tf#L128) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | +| [policy_list](variables.tf#L135) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L147) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [firewall_policies](outputs.tf#L16) | Map of firewall policy resources created in this folder. | | +| [firewall_policy_id](outputs.tf#L21) | Map of firewall policy ids created in this folder. | | +| [folder](outputs.tf#L26) | Folder resource. | | +| [id](outputs.tf#L31) | Folder id. | | +| [name](outputs.tf#L41) | Folder name. | | +| [sink_writer_identities](outputs.tf#L46) | Writer identities created for each sink. | | + + diff --git a/examples/guardrails/azuredevops/folder-factory/modules/folder/firewall-policies.tf b/examples/guardrails/azuredevops/folder-factory/modules/folder/firewall-policies.tf new file mode 100644 index 0000000..96224c5 --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/modules/folder/firewall-policies.tf @@ -0,0 +1,93 @@ +/** + * 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 + * + * 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 { + _factory_cidrs = try( + yamldecode(file(var.firewall_policy_factory.cidr_file)), {} + ) + _factory_name = ( + try(var.firewall_policy_factory.policy_name, null) == null + ? "factory" + : var.firewall_policy_factory.policy_name + ) + _factory_rules = try( + yamldecode(file(var.firewall_policy_factory.rules_file)), {} + ) + _factory_rules_parsed = { + for name, rule in local._factory_rules : name => merge(rule, { + ranges = flatten([ + for r in(rule.ranges == null ? [] : rule.ranges) : + lookup(local._factory_cidrs, trimprefix(r, "$"), r) + ]) + }) + } + _merged_rules = flatten([ + for policy, rules in local.firewall_policies : [ + for name, rule in rules : merge(rule, { + policy = policy + name = name + }) + ] + ]) + firewall_policies = merge(var.firewall_policies, ( + length(local._factory_rules) == 0 + ? {} + : { (local._factory_name) = local._factory_rules_parsed } + )) + firewall_rules = { + for r in local._merged_rules : "${r.policy}-${r.name}" => r + } +} + +resource "google_compute_firewall_policy" "policy" { + for_each = local.firewall_policies + short_name = each.key + parent = local.folder.id +} + +resource "google_compute_firewall_policy_rule" "rule" { + for_each = local.firewall_rules + firewall_policy = google_compute_firewall_policy.policy[each.value.policy].id + action = each.value.action + direction = each.value.direction + priority = try(each.value.priority, null) + target_resources = try(each.value.target_resources, null) + target_service_accounts = try(each.value.target_service_accounts, null) + enable_logging = try(each.value.logging, null) + # preview = each.value.preview + description = each.value.description + match { + src_ip_ranges = each.value.direction == "INGRESS" ? each.value.ranges : null + dest_ip_ranges = each.value.direction == "EGRESS" ? each.value.ranges : null + dynamic "layer4_configs" { + for_each = each.value.ports + iterator = port + content { + ip_protocol = port.key + ports = port.value + } + } + } +} + + +resource "google_compute_firewall_policy_association" "association" { + for_each = var.firewall_policy_association + name = replace(local.folder.id, "/", "-") + attachment_target = local.folder.id + firewall_policy = try(google_compute_firewall_policy.policy[each.value].id, each.value) +} + diff --git a/examples/guardrails/azuredevops/folder-factory/modules/folder/iam.tf b/examples/guardrails/azuredevops/folder-factory/modules/folder/iam.tf new file mode 100644 index 0000000..52886ba --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/modules/folder/iam.tf @@ -0,0 +1,40 @@ +/** + * 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 + * + * 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 IAM bindings, roles and audit logging resources. + +locals { + group_iam_roles = distinct(flatten(values(var.group_iam))) + group_iam = { + for r in local.group_iam_roles : r => [ + for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null + ] + } + iam = { + for role in distinct(concat(keys(var.iam), keys(local.group_iam))) : + role => concat( + try(var.iam[role], []), + try(local.group_iam[role], []) + ) + } +} + +resource "google_folder_iam_binding" "authoritative" { + for_each = local.iam + folder = local.folder.name + role = each.key + members = each.value +} diff --git a/examples/guardrails/azuredevops/folder-factory/modules/folder/logging.tf b/examples/guardrails/azuredevops/folder-factory/modules/folder/logging.tf new file mode 100644 index 0000000..d6a195e --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/modules/folder/logging.tf @@ -0,0 +1,91 @@ +/** + * 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 + * + * 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 Log sinks and supporting resources. + +locals { + sink_bindings = { + for type in ["bigquery", "pubsub", "logging", "storage"] : + type => { + for name, sink in var.logging_sinks : + name => sink + if sink.type == type + } + } +} + +resource "google_logging_folder_sink" "sink" { + for_each = var.logging_sinks + name = each.key + #description = "${each.key} (Terraform-managed)." + folder = local.folder.name + destination = "${each.value.type}.googleapis.com/${each.value.destination}" + filter = each.value.filter + include_children = each.value.include_children + + dynamic "exclusions" { + for_each = each.value.exclusions + iterator = exclusion + content { + name = exclusion.key + filter = exclusion.value + } + } + + depends_on = [ + google_folder_iam_binding.authoritative + ] +} + +resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { + for_each = local.sink_bindings["storage"] + bucket = each.value.destination + role = "roles/storage.objectCreator" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" { + for_each = local.sink_bindings["bigquery"] + project = split("/", each.value.destination)[1] + dataset_id = split("/", each.value.destination)[3] + role = "roles/bigquery.dataEditor" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" { + for_each = local.sink_bindings["pubsub"] + project = split("/", each.value.destination)[1] + topic = split("/", each.value.destination)[3] + role = "roles/pubsub.publisher" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_project_iam_member" "bucket-sinks-binding" { + for_each = local.sink_bindings["logging"] + project = split("/", each.value.destination)[1] + role = "roles/logging.bucketWriter" + member = google_logging_folder_sink.sink[each.key].writer_identity + # TODO(jccb): use a condition to limit writer-identity only to this + # bucket +} + +resource "google_logging_folder_exclusion" "logging-exclusion" { + for_each = var.logging_exclusions + name = each.key + folder = local.folder.name + description = "${each.key} (Terraform-managed)." + filter = each.value +} diff --git a/examples/guardrails/azuredevops/folder-factory/modules/folder/main.tf b/examples/guardrails/azuredevops/folder-factory/modules/folder/main.tf new file mode 100644 index 0000000..5d285d2 --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/modules/folder/main.tf @@ -0,0 +1,43 @@ +/** + * 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 + * + * 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 { + folder = ( + var.folder_create + ? try(google_folder.folder.0, null) + : try(data.google_folder.folder.0, null) + ) +} + +data "google_folder" "folder" { + count = var.folder_create ? 0 : 1 + folder = var.id +} + +resource "google_folder" "folder" { + count = var.folder_create ? 1 : 0 + display_name = var.name + parent = var.parent +} + +resource "google_essential_contacts_contact" "contact" { + provider = google-beta + for_each = var.contacts + parent = local.folder.name + email = each.key + language_tag = "en" + notification_category_subscriptions = each.value +} diff --git a/examples/guardrails/azuredevops/folder-factory/modules/folder/organization-policies.tf b/examples/guardrails/azuredevops/folder-factory/modules/folder/organization-policies.tf new file mode 100644 index 0000000..177a3d8 --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/modules/folder/organization-policies.tf @@ -0,0 +1,90 @@ +/** + * 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 + * + * 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 Folder-level organization policies. + +resource "google_folder_organization_policy" "boolean" { + for_each = var.policy_boolean + folder = local.folder.name + constraint = each.key + + dynamic "boolean_policy" { + for_each = each.value == null ? [] : [each.value] + iterator = policy + content { + enforced = policy.value + } + } + + dynamic "restore_policy" { + for_each = each.value == null ? [""] : [] + content { + default = true + } + } +} + +resource "google_folder_organization_policy" "list" { + for_each = var.policy_list + folder = local.folder.name + constraint = each.key + + dynamic "list_policy" { + for_each = each.value.status == null ? [] : [each.value] + iterator = policy + content { + inherit_from_parent = policy.value.inherit_from_parent + suggested_value = policy.value.suggested_value + dynamic "allow" { + for_each = policy.value.status ? [""] : [] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + dynamic "deny" { + for_each = policy.value.status ? [] : [""] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + } + } + + dynamic "restore_policy" { + for_each = each.value.status == null ? [true] : [] + content { + default = true + } + } +} diff --git a/examples/guardrails/azuredevops/folder-factory/modules/folder/outputs.tf b/examples/guardrails/azuredevops/folder-factory/modules/folder/outputs.tf new file mode 100644 index 0000000..37babc6 --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/modules/folder/outputs.tf @@ -0,0 +1,51 @@ +/** + * 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 + * + * 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. + */ +output "firewall_policies" { + description = "Map of firewall policy resources created in this folder." + value = { for k, v in google_compute_firewall_policy.policy : k => v } +} + +output "firewall_policy_id" { + description = "Map of firewall policy ids created in this folder." + value = { for k, v in google_compute_firewall_policy.policy : k => v.id } +} + +output "folder" { + description = "Folder resource." + value = local.folder +} + +output "id" { + description = "Folder id." + value = local.folder.name + depends_on = [ + google_folder_iam_binding.authoritative, + google_folder_organization_policy.boolean, + google_folder_organization_policy.list + ] +} + +output "name" { + description = "Folder name." + value = local.folder.display_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 + } +} diff --git a/examples/guardrails/azuredevops/folder-factory/modules/folder/tags.tf b/examples/guardrails/azuredevops/folder-factory/modules/folder/tags.tf new file mode 100644 index 0000000..2cd2f2f --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/modules/folder/tags.tf @@ -0,0 +1,21 @@ +/** + * 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 + * + * 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. + */ + +resource "google_tags_tag_binding" "binding" { + for_each = coalesce(var.tag_bindings, {}) + parent = "//cloudresourcemanager.googleapis.com/${local.folder.id}" + tag_value = each.value +} diff --git a/examples/guardrails/azuredevops/folder-factory/modules/folder/variables.tf b/examples/guardrails/azuredevops/folder-factory/modules/folder/variables.tf new file mode 100644 index 0000000..a3f32e3 --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/modules/folder/variables.tf @@ -0,0 +1,151 @@ +/** + * 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 + * + * 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. + */ + +variable "contacts" { + description = "List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES." + type = map(list(string)) + default = {} + nullable = false +} + +variable "firewall_policies" { + description = "Hierarchical firewall policies created in this folder." + type = map(map(object({ + action = string + description = string + direction = string + logging = bool + ports = map(list(string)) + priority = number + ranges = list(string) + target_resources = list(string) + target_service_accounts = list(string) + }))) + default = {} + nullable = false +} + +variable "firewall_policy_association" { + description = "The hierarchical firewall policy to associate to this folder. Must be either a key in the `firewall_policies` map or the id of a policy defined somewhere else." + type = map(string) + default = {} + nullable = false +} + +variable "firewall_policy_factory" { + description = "Configuration for the firewall policy factory." + type = object({ + cidr_file = string + policy_name = string + rules_file = string + }) + default = null +} + +variable "folder_create" { + description = "Create folder. When set to false, uses id to reference an existing folder." + type = bool + default = true +} + +variable "group_iam" { + description = "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." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam" { + description = "IAM bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "id" { + description = "Folder ID in case you use folder_create=false." + type = string + default = null +} + +variable "logging_exclusions" { + description = "Logging exclusions for this folder in the form {NAME -> FILTER}." + type = map(string) + default = {} + nullable = false +} + +variable "logging_sinks" { + description = "Logging sinks to create for this folder." + type = map(object({ + destination = string + type = string + filter = string + include_children = bool + # TODO exclusions also support description and disabled + exclusions = map(string) + })) + validation { + condition = alltrue([ + for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } + default = {} + nullable = false +} + +variable "name" { + description = "Folder name." + type = string + default = null +} + +variable "parent" { + description = "Parent in folders/folder_id or organizations/org_id format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "policy_boolean" { + description = "Map of boolean org policies and enforcement value, set value to null for policy restore." + type = map(bool) + default = {} + nullable = false +} + +variable "policy_list" { + description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." + type = map(object({ + inherit_from_parent = bool + suggested_value = string + status = bool + values = list(string) + })) + default = {} + nullable = false +} + +variable "tag_bindings" { + description = "Tag bindings for this folder, in key => tag value id format." + type = map(string) + default = null +} diff --git a/examples/guardrails/azuredevops/folder-factory/modules/folder/versions.tf b/examples/guardrails/azuredevops/folder-factory/modules/folder/versions.tf new file mode 100644 index 0000000..e72a780 --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/modules/folder/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.1.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/azuredevops/folder-factory/outputs.tf b/examples/guardrails/azuredevops/folder-factory/outputs.tf new file mode 100644 index 0000000..4fc1488 --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/outputs.tf @@ -0,0 +1,20 @@ +/** + * 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 + * + * 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. + */ + + output "folders" { + description = "Created folders." + value = module.folder + } \ No newline at end of file diff --git a/examples/guardrails/azuredevops/folder-factory/provider.tf b/examples/guardrails/azuredevops/folder-factory/provider.tf new file mode 100644 index 0000000..d442bca --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/provider.tf @@ -0,0 +1,37 @@ +/** + * 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 + * + * 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. + */ + +terraform { + /*backend "gcs" { + bucket = "terraform-backend-bucket-azuredevops" + prefix = "state" + }*/ + required_providers { + google = { + source = "hashicorp/google" + version = "~>4.11" + } + } +} + +provider "google" { + project = "azuretogcp-374609" + region = "us-central1" +} + +provider "google-beta" { + +} diff --git a/examples/guardrails/azuredevops/folder-factory/variables.tf b/examples/guardrails/azuredevops/folder-factory/variables.tf new file mode 100644 index 0000000..11a2ddf --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/variables.tf @@ -0,0 +1,15 @@ +/** + * 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 + * + * 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. + */ diff --git a/examples/guardrails/azuredevops/project-factory/.github/workflows/terraform-deployment.yml b/examples/guardrails/azuredevops/project-factory/.github/workflows/terraform-deployment.yml new file mode 100644 index 0000000..4099678 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/.github/workflows/terraform-deployment.yml @@ -0,0 +1,94 @@ +# 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 +# +# 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. + +name: 'Cloud Deployment' + +on: + push: + branches: + - main + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + FOLDER: ${{ secrets.FOLDER }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} + +# OR SET MANUALLY +# +#env: +# STATE_BUCKET: 'XXXX' +# FOLDER: 'folders/XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan -var "folder=$FOLDER" + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -var "folder=$FOLDER" -auto-approve + + diff --git a/examples/guardrails/azuredevops/project-factory/.gitignore b/examples/guardrails/azuredevops/project-factory/.gitignore new file mode 100644 index 0000000..a5d8c0e --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.terraform +.terraform.lock.hcl diff --git a/examples/guardrails/azuredevops/project-factory/README.md b/examples/guardrails/azuredevops/project-factory/README.md new file mode 100644 index 0000000..f901e7f --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/README.md @@ -0,0 +1,75 @@ +# Project Factory + +This is a template for a DevOps project factory. + +It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory) and is intended to house the projects of a specified folder: + +Overview + +Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. + +![Folder Factory](https://user-images.githubusercontent.com/94000358/169809882-f5ff9fb1-d037-49de-8c2c-bf0d457b662f.png) + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: + +Secret settings + +or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: +``` +env: + STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + FOLDER: 'folders/XXXX' + # The folder under which the projects should be created + WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' + # The workload identity provider that should be used for this repository. + SERVICE_ACCOUNT: 'XXXX@XXXX' + # The service account that should be used for this repository. +``` + +## Setting up projects + +The project factory will: +- create a service account with defined rights +- create a project within the folder +- connect the service account to the Github repository informantion + +It uses YAML configuration files for every project with the following sample structure: +``` +billing_account_id: XXXXXX-XXXXXX-XXXXXX +roles: + - roles/viewer + - roles/iam.serviceAccountUser + - roles/iam.securityReviewer + - roles/monitoring.viewer + - roles/monitoring.editor + - roles/monitoring.alertPolicyViewer + - roles/monitoring.alertPolicyEditor + - roles/monitoring.dashboardViewer + - roles/monitoring.dashboardEditor + - roles/monitoring.notificationChannelViewer + - roles/monitoring.notificationChannelEditor + - roles/monitoring.servicesViewer + - roles/monitoring.servicesEditor + - roles/monitoring.uptimeCheckConfigViewer + - roles/monitoring.uptimeCheckConfigEditor + - roles/secretmanager.viewer + - roles/secretmanager.secretVersionManager + - roles/secretmanager.admin + - roles/storage.admin + - roles/storage.objectAdmin + - roles/storage.objectCreator + - roles/storage.objectViewer +repo_provider: github +repo_name: devops-governance/skunkworks +repo_branch: dev +``` + +Every project is defined with its own file located in the [Project Folder](data/projects). diff --git a/examples/guardrails/azuredevops/project-factory/azure-pipeline.yml b/examples/guardrails/azuredevops/project-factory/azure-pipeline.yml new file mode 100644 index 0000000..814a166 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/azure-pipeline.yml @@ -0,0 +1,124 @@ +name: $(Date:yyyyMMdd)$(Rev:.r) + +trigger: + - none +pool: + vmImage: ubuntu-latest +stages: + - stage: auth + displayName: "GCP WIF Auth" + jobs: + - job: governance_pipeline + timeoutInMinutes: 20 + steps: + - task: charleszipp.azure-pipelines-tasks-terraform.azure-pipelines-tasks-terraform-installer.TerraformInstaller@0 + displayName: install terraform + inputs: + terraformVersion: latest + + # AzureCLI task to retrieve an Azure token for the Service Principal and then exchanges it against a Service Account token using Workload Identity Federation + + - task: AzureCLI@2 + displayName: get access token + inputs: + azureSubscription: 'azuretogcp' + scriptType: 'bash' + scriptLocation: 'inlineScript' + inlineScript: | + SUBJECT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt" + SUBJECT_TOKEN=$(az account get-access-token --query accessToken --output tsv) + STS_TOKEN=$(curl --silent -0 -X POST https://sts.googleapis.com/v1/token \ + -H 'Content-Type: text/json; charset=utf-8' \ + -d @- < yamldecode(file("./data/projects/${f}")) + } +} + +module "project" { + source = "./modules/project_plus" + for_each = local.projects + team = each.key + repo_sub = "${each.value.repo_provider == "gitlab" ? "project_path:${each.value.repo_name}:ref_type:branch:ref:${each.value.repo_branch}" : "repo:${each.value.repo_name}:ref:refs/heads/${each.value.repo_branch}"}" + repo_provider = each.value.repo_provider + billing_account = each.value.billing_account_id + folder = var.folder + roles = try(each.value.roles, []) + wif-pool = "${each.value.repo_provider == "gitlab" ? google_iam_workload_identity_pool.wif-pool-gitlab.name : google_iam_workload_identity_pool.wif-pool-github.name}" + depends_on = [google_iam_workload_identity_pool.wif-pool-github,google_iam_workload_identity_pool.wif-pool-gitlab] +} diff --git a/examples/guardrails/azuredevops/project-factory/modules/project/README.md b/examples/guardrails/azuredevops/project-factory/modules/project/README.md new file mode 100644 index 0000000..e4f2139 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project/README.md @@ -0,0 +1,308 @@ +# Project Module + +## Examples + +### Minimal example with IAM + +```hcl +locals { + gke_service_account = "my_gke_service_account" +} + +module "project" { + source = "./modules/project" + billing_account = "123456-123456-123456" + name = "project-example" + parent = "folders/1234567890" + prefix = "foo" + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + iam = { + "roles/container.hostServiceAgentUser" = [ + "serviceAccount:${local.gke_service_account}" + ] + } +} +# tftest modules=1 resources=4 +``` + +### Minimal example with IAM additive roles + +```hcl +module "project" { + source = "./modules/project" + name = "project-example" + + iam_additive = { + "roles/viewer" = [ + "group:one@example.org", "group:two@xample.org" + ], + "roles/storage.objectAdmin" = [ + "group:two@example.org" + ], + "roles/owner" = [ + "group:three@example.org" + ], + } +} +# tftest modules=1 resources=5 +``` + +### Shared VPC service + +```hcl +module "project" { + source = "./modules/project" + name = "project-example" + + shared_vpc_service_config = { + attach = true + host_project = "my-host-project" + service_identity_iam = { + "roles/compute.networkUser" = [ + "cloudservices", "container-engine" + ] + "roles/vpcaccess.user" = [ + "cloudrun" + ] + "roles/container.hostServiceAgentUser" = [ + "container-engine" + ] + } + } +} +# tftest modules=1 resources=6 +``` + +### Organization policies + +```hcl +module "project" { + source = "./modules/project" + billing_account = "123456-123456-123456" + name = "project-example" + parent = "folders/1234567890" + prefix = "foo" + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] + policy_boolean = { + "constraints/compute.disableGuestAttributesAccess" = true + "constraints/compute.skipDefaultNetworkCreation" = true + } + policy_list = { + "constraints/compute.trustedImageProjects" = { + inherit_from_parent = null + suggested_value = null + status = true + values = ["projects/my-project"] + } + } +} +# tftest modules=1 resources=6 +``` + +## Logging Sinks + +```hcl +module "gcs" { + source = "./modules/gcs" + project_id = var.project_id + name = "gcs_sink" + force_destroy = true +} + +module "dataset" { + source = "./modules/bigquery-dataset" + project_id = var.project_id + id = "bq_sink" +} + +module "pubsub" { + source = "./modules/pubsub" + project_id = var.project_id + name = "pubsub_sink" +} + +module "bucket" { + source = "./modules/logging-bucket" + parent_type = "project" + parent = "my-project" + id = "bucket" +} + +module "project-host" { + source = "./modules/project" + name = "my-project" + billing_account = "123456-123456-123456" + parent = "folders/1234567890" + logging_sinks = { + warnings = { + type = "storage" + destination = module.gcs.name + filter = "severity=WARNING" + iam = false + unique_writer = false + exclusions = {} + } + info = { + type = "bigquery" + destination = module.dataset.id + filter = "severity=INFO" + iam = false + unique_writer = false + exclusions = {} + } + notice = { + type = "pubsub" + destination = module.pubsub.id + filter = "severity=NOTICE" + iam = true + unique_writer = false + exclusions = {} + } + debug = { + type = "logging" + destination = module.bucket.id + filter = "severity=DEBUG" + iam = true + unique_writer = false + exclusions = { + no-compute = "logName:compute" + } + } + } + logging_exclusions = { + no-gce-instances = "resource.type=gce_instance" + } +} +# tftest modules=5 resources=12 +``` + +## Cloud KMS encryption keys + +```hcl +module "project" { + source = "./modules/project" + name = "my-project" + billing_account = "123456-123456-123456" + prefix = "foo" + services = [ + "compute.googleapis.com", + "storage.googleapis.com" + ] + service_encryption_key_ids = { + compute = [ + "projects/kms-central-prj/locations/europe-west3/keyRings/my-keyring/cryptoKeys/europe3-gce", + "projects/kms-central-prj/locations/europe-west4/keyRings/my-keyring/cryptoKeys/europe4-gce" + ] + storage = [ + "projects/kms-central-prj/locations/europe/keyRings/my-keyring/cryptoKeys/europe-gcs" + ] + } +} +# tftest modules=1 resources=7 +``` + +## 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. + +```hcl +module "org" { + source = "./modules/organization" + organization_id = var.organization_id + tags = { + environment = { + description = "Environment specification." + iam = null + values = { + dev = null + prod = null + } + } + } +} + +module "project" { + source = "./modules/project" + name = "test-project" + tag_bindings = { + env-prod = module.org.tag_values["environment/prod"].id + foo = "tagValues/12345678" + } +} +# tftest modules=2 resources=6 +``` + + + + +## Files + +| name | description | resources | +|---|---|---| +| [iam.tf](./iam.tf) | Generic and OSLogin-specific IAM bindings and roles. | google_project_iam_binding · google_project_iam_custom_role · google_project_iam_member | +| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_project_exclusion · google_logging_project_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | +| [main.tf](./main.tf) | Module-level locals and resources. | google_compute_project_metadata_item · google_essential_contacts_contact · google_monitoring_monitored_project · google_project · google_project_service · google_resource_manager_lien | +| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_project_organization_policy | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [service-accounts.tf](./service-accounts.tf) | Service identities and supporting resources. | google_kms_crypto_key_iam_member · google_project_service_identity | +| [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project · google_compute_shared_vpc_service_project · google_project_iam_member | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding | +| [variables.tf](./variables.tf) | Module variables. | | +| [versions.tf](./versions.tf) | Version pins. | | +| [vpc-sc.tf](./vpc-sc.tf) | VPC-SC project-level perimeter configuration. | google_access_context_manager_service_perimeter_resource | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [name](variables.tf#L125) | Project name and id suffix. | string | ✓ | | +| [auto_create_network](variables.tf#L17) | Whether to create the default network for the project. | bool | | false | +| [billing_account](variables.tf#L23) | Billing account id. | string | | null | +| [contacts](variables.tf#L29) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | +| [custom_roles](variables.tf#L36) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| [descriptive_name](variables.tf#L43) | Name of the project name. Used for project name instead of `name` variable. | string | | null | +| [group_iam](variables.tf#L49) | 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. | map(list(string)) | | {} | +| [iam](variables.tf#L56) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive](variables.tf#L63) | IAM additive bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive_members](variables.tf#L70) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | map(list(string)) | | {} | +| [labels](variables.tf#L76) | Resource labels. | map(string) | | {} | +| [lien_reason](variables.tf#L83) | If non-empty, creates a project lien with this description. | string | | "" | +| [logging_exclusions](variables.tf#L89) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L96) | Logging sinks to create for this project. | map(object({…})) | | {} | +| [metric_scopes](variables.tf#L118) | List of projects that will act as metric scopes for this project. | list(string) | | [] | +| [oslogin](variables.tf#L130) | Enable OS Login. | bool | | false | +| [oslogin_admins](variables.tf#L136) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | +| [oslogin_users](variables.tf#L144) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | +| [parent](variables.tf#L151) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [policy_boolean](variables.tf#L161) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | +| [policy_list](variables.tf#L168) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | +| [prefix](variables.tf#L180) | Prefix used to generate project id and name. | string | | null | +| [project_create](variables.tf#L186) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | +| [service_config](variables.tf#L192) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L204) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | +| [service_perimeter_bridges](variables.tf#L211) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | +| [service_perimeter_standard](variables.tf#L218) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | +| [services](variables.tf#L224) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L230) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L239) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | +| [skip_delete](variables.tf#L249) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [tag_bindings](variables.tf#L255) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [custom_roles](outputs.tf#L17) | Ids of the created custom roles. | | +| [name](outputs.tf#L25) | Project name. | | +| [number](outputs.tf#L38) | Project number. | | +| [project_id](outputs.tf#L51) | Project id. | | +| [service_accounts](outputs.tf#L66) | Product robot service accounts in project. | | +| [sink_writer_identities](outputs.tf#L82) | Writer identities created for each sink. | | + + diff --git a/examples/guardrails/azuredevops/project-factory/modules/project/iam.tf b/examples/guardrails/azuredevops/project-factory/modules/project/iam.tf new file mode 100644 index 0000000..69925cc --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project/iam.tf @@ -0,0 +1,115 @@ +/** + * 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 + * + * 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 Generic and OSLogin-specific IAM bindings and roles. + +# IAM notes: +# - external users need to have accepted the invitation email to join +# - oslogin roles also require role to list instances +# - additive (non-authoritative) roles might fail due to dynamic values + +locals { + _group_iam_roles = distinct(flatten(values(var.group_iam))) + _group_iam = { + for r in local._group_iam_roles : r => [ + 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( + try(var.iam[role], []), + 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_project_iam_custom_role" "roles" { + for_each = var.custom_roles + project = local.project.project_id + role_id = each.key + title = "Custom role ${each.key}" + description = "Terraform-managed." + permissions = each.value +} + +resource "google_project_iam_binding" "authoritative" { + for_each = local.iam + project = local.project.project_id + role = each.key + members = each.value + depends_on = [ + google_project_service.project_services, + google_project_iam_custom_role.roles + ] +} + +resource "google_project_iam_member" "additive" { + for_each = ( + length(var.iam_additive) + length(var.iam_additive_members) > 0 + ? local.iam_additive + : {} + ) + project = local.project.project_id + role = each.value.role + member = each.value.member + depends_on = [ + google_project_service.project_services, + google_project_iam_custom_role.roles + ] +} + +resource "google_project_iam_member" "oslogin_iam_serviceaccountuser" { + for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([]) + project = local.project.project_id + role = "roles/iam.serviceAccountUser" + member = each.value +} + +resource "google_project_iam_member" "oslogin_compute_viewer" { + for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([]) + project = local.project.project_id + role = "roles/compute.viewer" + member = each.value +} + +resource "google_project_iam_member" "oslogin_admins" { + for_each = var.oslogin ? toset(var.oslogin_admins) : toset([]) + project = local.project.project_id + role = "roles/compute.osAdminLogin" + member = each.value +} + +resource "google_project_iam_member" "oslogin_users" { + for_each = var.oslogin ? toset(var.oslogin_users) : toset([]) + project = local.project.project_id + role = "roles/compute.osLogin" + member = each.value +} diff --git a/examples/guardrails/azuredevops/project-factory/modules/project/logging.tf b/examples/guardrails/azuredevops/project-factory/modules/project/logging.tf new file mode 100644 index 0000000..04d7abf --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project/logging.tf @@ -0,0 +1,91 @@ +/** + * 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 + * + * 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 Log sinks and supporting resources. + +locals { + sink_bindings = { + for type in ["bigquery", "pubsub", "logging", "storage"] : + type => { + for name, sink in var.logging_sinks : + name => sink if sink.iam && sink.type == type + } + } +} + +resource "google_logging_project_sink" "sink" { + for_each = var.logging_sinks + name = each.key + #description = "${each.key} (Terraform-managed)." + project = local.project.project_id + destination = "${each.value.type}.googleapis.com/${each.value.destination}" + filter = each.value.filter + unique_writer_identity = each.value.unique_writer + + dynamic "exclusions" { + for_each = each.value.exclusions + iterator = exclusion + content { + name = exclusion.key + filter = exclusion.value + } + } + + depends_on = [ + google_project_iam_binding.authoritative, + google_project_iam_member.additive + ] +} + +resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { + for_each = local.sink_bindings["storage"] + bucket = each.value.destination + role = "roles/storage.objectCreator" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" { + for_each = local.sink_bindings["bigquery"] + project = split("/", each.value.destination)[1] + dataset_id = split("/", each.value.destination)[3] + role = "roles/bigquery.dataEditor" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" { + for_each = local.sink_bindings["pubsub"] + project = split("/", each.value.destination)[1] + topic = split("/", each.value.destination)[3] + role = "roles/pubsub.publisher" + member = google_logging_project_sink.sink[each.key].writer_identity +} + +resource "google_project_iam_member" "bucket-sinks-binding" { + for_each = local.sink_bindings["logging"] + project = split("/", each.value.destination)[1] + role = "roles/logging.bucketWriter" + member = google_logging_project_sink.sink[each.key].writer_identity + # TODO(jccb): use a condition to limit writer-identity only to this + # bucket +} + +resource "google_logging_project_exclusion" "logging-exclusion" { + for_each = var.logging_exclusions + name = each.key + project = local.project.project_id + description = "${each.key} (Terraform-managed)." + filter = each.value +} diff --git a/examples/guardrails/azuredevops/project-factory/modules/project/main.tf b/examples/guardrails/azuredevops/project-factory/modules/project/main.tf new file mode 100644 index 0000000..9f0dff4 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project/main.tf @@ -0,0 +1,95 @@ +/** + * 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 + * + * 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 { + descriptive_name = ( + var.descriptive_name != null ? var.descriptive_name : "${local.prefix}${var.name}" + ) + parent_type = var.parent == null ? null : split("/", var.parent)[0] + parent_id = var.parent == null ? null : split("/", var.parent)[1] + prefix = var.prefix == null ? "" : "${var.prefix}-" + project = ( + var.project_create ? + { + project_id = try(google_project.project.0.project_id, null) + number = try(google_project.project.0.number, null) + name = try(google_project.project.0.name, null) + } + : { + project_id = "${local.prefix}${var.name}" + number = try(data.google_project.project.0.number, null) + name = try(data.google_project.project.0.name, null) + } + ) +} + +data "google_project" "project" { + count = var.project_create ? 0 : 1 + project_id = "${local.prefix}${var.name}" +} + +resource "google_project" "project" { + count = var.project_create ? 1 : 0 + org_id = local.parent_type == "organizations" ? local.parent_id : null + folder_id = local.parent_type == "folders" ? local.parent_id : null + project_id = "${local.prefix}${var.name}" + name = local.descriptive_name + billing_account = var.billing_account + auto_create_network = var.auto_create_network + labels = var.labels + skip_delete = var.skip_delete +} + +resource "google_project_service" "project_services" { + for_each = toset(var.services) + project = local.project.project_id + service = each.value + disable_on_destroy = var.service_config.disable_on_destroy + disable_dependent_services = var.service_config.disable_dependent_services +} + +resource "google_compute_project_metadata_item" "oslogin_meta" { + count = var.oslogin ? 1 : 0 + project = local.project.project_id + key = "enable-oslogin" + value = "TRUE" + # depend on services or it will fail on destroy + depends_on = [google_project_service.project_services] +} + +resource "google_resource_manager_lien" "lien" { + count = var.lien_reason != "" ? 1 : 0 + parent = "projects/${local.project.number}" + restrictions = ["resourcemanager.projects.delete"] + origin = "created-by-terraform" + reason = var.lien_reason +} + +resource "google_essential_contacts_contact" "contact" { + provider = google-beta + for_each = var.contacts + parent = "projects/${local.project.project_id}" + email = each.key + language_tag = "en" + notification_category_subscriptions = each.value +} + +resource "google_monitoring_monitored_project" "primary" { + provider = google-beta + for_each = toset(var.metric_scopes) + metrics_scope = each.value + name = local.project.project_id +} diff --git a/examples/guardrails/azuredevops/project-factory/modules/project/organization-policies.tf b/examples/guardrails/azuredevops/project-factory/modules/project/organization-policies.tf new file mode 100644 index 0000000..6870754 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project/organization-policies.tf @@ -0,0 +1,90 @@ +/** + * 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 + * + * 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 Project-level organization policies. + +resource "google_project_organization_policy" "boolean" { + for_each = var.policy_boolean + project = local.project.project_id + constraint = each.key + + dynamic "boolean_policy" { + for_each = each.value == null ? [] : [each.value] + iterator = policy + content { + enforced = policy.value + } + } + + dynamic "restore_policy" { + for_each = each.value == null ? [""] : [] + content { + default = true + } + } +} + +resource "google_project_organization_policy" "list" { + for_each = var.policy_list + project = local.project.project_id + constraint = each.key + + dynamic "list_policy" { + for_each = each.value.status == null ? [] : [each.value] + iterator = policy + content { + inherit_from_parent = policy.value.inherit_from_parent + suggested_value = policy.value.suggested_value + dynamic "allow" { + for_each = policy.value.status ? [""] : [] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + dynamic "deny" { + for_each = policy.value.status ? [] : [""] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + } + } + + dynamic "restore_policy" { + for_each = each.value.status == null ? [true] : [] + content { + default = true + } + } +} diff --git a/examples/guardrails/azuredevops/project-factory/modules/project/outputs.tf b/examples/guardrails/azuredevops/project-factory/modules/project/outputs.tf new file mode 100644 index 0000000..10d0e55 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project/outputs.tf @@ -0,0 +1,87 @@ +/** + * 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 + * + * 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. + */ + +output "custom_roles" { + description = "Ids of the created custom roles." + value = { + for name, role in google_project_iam_custom_role.roles : + name => role.id + } +} + +output "name" { + description = "Project name." + value = local.project.name + depends_on = [ + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "number" { + description = "Project number." + value = local.project.number + depends_on = [ + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "project_id" { + description = "Project id." + value = "${local.prefix}${var.name}" + depends_on = [ + google_project.project, + data.google_project.project, + google_project_organization_policy.boolean, + google_project_organization_policy.list, + google_project_service.project_services, + google_compute_shared_vpc_service_project.service_projects, + google_project_iam_member.shared_vpc_host_robots, + google_kms_crypto_key_iam_member.service_identity_cmek + ] +} + +output "service_accounts" { + description = "Product robot service accounts in project." + value = { + cloud_services = local.service_account_cloud_services + default = local.service_accounts_default + robots = local.service_accounts_robots + } + depends_on = [ + google_project_service.project_services, + google_kms_crypto_key_iam_member.service_identity_cmek, + google_project_service_identity.jit_si, + data.google_bigquery_default_service_account.bq_sa, + data.google_storage_project_service_account.gcs_sa + ] +} + +output "sink_writer_identities" { + description = "Writer identities created for each sink." + value = { + for name, sink in google_logging_project_sink.sink : name => sink.writer_identity + } +} diff --git a/examples/guardrails/azuredevops/project-factory/modules/project/service-accounts.tf b/examples/guardrails/azuredevops/project-factory/modules/project/service-accounts.tf new file mode 100644 index 0000000..3423524 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project/service-accounts.tf @@ -0,0 +1,113 @@ +/** + * 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 + * + * 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 Service identities and supporting resources. + +locals { + _service_accounts_cmek_service_dependencies = { + "composer" : [ + "composer", + "artifactregistry", "container-engine", "compute", "pubsub", "storage" + ] + "dataflow" : ["dataflow", "compute"] + } + _service_accounts_robot_services = { + artifactregistry = "service-%s@gcp-sa-artifactregistry" + bq = "bq-%s@bigquery-encryption" + cloudasset = "service-%s@gcp-sa-cloudasset" + cloudbuild = "service-%s@gcp-sa-cloudbuild" + cloudfunctions = "service-%s@gcf-admin-robot" + cloudrun = "service-%s@serverless-robot-prod" + composer = "service-%s@cloudcomposer-accounts" + compute = "service-%s@compute-system" + container-engine = "service-%s@container-engine-robot" + containerregistry = "service-%s@containerregistry" + dataflow = "service-%s@dataflow-service-producer-prod" + dataproc = "service-%s@dataproc-accounts" + gae-flex = "service-%s@gae-api-prod" + # TODO: deprecate gcf + gcf = "service-%s@gcf-admin-robot" + pubsub = "service-%s@gcp-sa-pubsub" + secretmanager = "service-%s@gcp-sa-secretmanager" + storage = "service-%s@gs-project-accounts" + } + service_accounts_default = { + compute = "${local.project.number}-compute@developer.gserviceaccount.com" + gae = "${local.project.project_id}@appspot.gserviceaccount.com" + } + service_account_cloud_services = ( + "${local.project.number}@cloudservices.gserviceaccount.com" + ) + service_accounts_robots = { + for k, v in local._service_accounts_robot_services : + k => "${format(v, local.project.number)}.iam.gserviceaccount.com" + } + service_accounts_jit_services = [ + "secretmanager.googleapis.com", + "pubsub.googleapis.com", + "cloudasset.googleapis.com" + ] + service_accounts_cmek_service_keys = distinct(flatten([ + for s in keys(var.service_encryption_key_ids) : [ + for ss in try(local._service_accounts_cmek_service_dependencies[s], [s]) : [ + for key in var.service_encryption_key_ids[s] : { + service = ss + key = key + } if key != null + ] + ] + ])) +} + +data "google_storage_project_service_account" "gcs_sa" { + count = contains(var.services, "storage.googleapis.com") ? 1 : 0 + project = local.project.project_id + depends_on = [google_project_service.project_services] +} + +data "google_bigquery_default_service_account" "bq_sa" { + count = contains(var.services, "bigquery.googleapis.com") ? 1 : 0 + project = local.project.project_id + depends_on = [google_project_service.project_services] +} + +# Secret Manager SA created just in time, we need to trigger the creation. +resource "google_project_service_identity" "jit_si" { + for_each = setintersection(var.services, local.service_accounts_jit_services) + provider = google-beta + project = local.project.project_id + service = each.value + depends_on = [google_project_service.project_services] +} + +resource "google_kms_crypto_key_iam_member" "service_identity_cmek" { + for_each = { + for service_key in local.service_accounts_cmek_service_keys : + "${service_key.service}.${service_key.key}" => service_key + if service_key != service_key.key + } + crypto_key_id = each.value.key + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + member = "serviceAccount:${local.service_accounts_robots[each.value.service]}" + depends_on = [ + google_project.project, + google_project_service.project_services, + google_project_service_identity.jit_si, + data.google_bigquery_default_service_account.bq_sa, + data.google_project.project, + data.google_storage_project_service_account.gcs_sa, + ] +} diff --git a/examples/guardrails/azuredevops/project-factory/modules/project/shared-vpc.tf b/examples/guardrails/azuredevops/project-factory/modules/project/shared-vpc.tf new file mode 100644 index 0000000..9c7bd71 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project/shared-vpc.tf @@ -0,0 +1,75 @@ +/** + * 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 + * + * 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 Shared VPC project-level configuration. + +locals { + # compute the host project IAM bindings for this project's service identities + _svpc_service_identity_iam = coalesce( + local.svpc_service_config.service_identity_iam, {} + ) + _svpc_service_iam = flatten([ + for role, services in local._svpc_service_identity_iam : [ + for service in services : { role = role, service = service } + ] + ]) + svpc_host_config = { + enabled = coalesce( + try(var.shared_vpc_host_config.enabled, null), false + ) + service_projects = coalesce( + try(var.shared_vpc_host_config.service_projects, null), [] + ) + } + svpc_service_config = coalesce(var.shared_vpc_service_config, { + host_project = null, service_identity_iam = {} + }) + svpc_service_iam = { + for b in local._svpc_service_iam : "${b.role}:${b.service}" => b + } +} + +resource "google_compute_shared_vpc_host_project" "shared_vpc_host" { + provider = google-beta + count = local.svpc_host_config.enabled ? 1 : 0 + project = local.project.project_id +} + +resource "google_compute_shared_vpc_service_project" "service_projects" { + provider = google-beta + for_each = toset(local.svpc_host_config.service_projects) + host_project = local.project.project_id + service_project = each.value + depends_on = [google_compute_shared_vpc_host_project.shared_vpc_host] +} + +resource "google_compute_shared_vpc_service_project" "shared_vpc_service" { + provider = google-beta + count = local.svpc_service_config.host_project != null ? 1 : 0 + host_project = var.shared_vpc_service_config.host_project + service_project = local.project.project_id +} + +resource "google_project_iam_member" "shared_vpc_host_robots" { + for_each = local.svpc_service_iam + project = var.shared_vpc_service_config.host_project + role = each.value.role + member = ( + each.value.service == "cloudservices" + ? "serviceAccount:${local.service_account_cloud_services}" + : "serviceAccount:${local.service_accounts_robots[each.value.service]}" + ) +} diff --git a/examples/guardrails/azuredevops/project-factory/modules/project/tags.tf b/examples/guardrails/azuredevops/project-factory/modules/project/tags.tf new file mode 100644 index 0000000..683143b --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project/tags.tf @@ -0,0 +1,21 @@ +/** + * 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 + * + * 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. + */ + +resource "google_tags_tag_binding" "binding" { + for_each = coalesce(var.tag_bindings, {}) + parent = "//cloudresourcemanager.googleapis.com/projects/${local.project.number}" + tag_value = each.value +} diff --git a/examples/guardrails/azuredevops/project-factory/modules/project/variables.tf b/examples/guardrails/azuredevops/project-factory/modules/project/variables.tf new file mode 100644 index 0000000..578f9d2 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project/variables.tf @@ -0,0 +1,259 @@ +/** + * 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 + * + * 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. + */ + +variable "auto_create_network" { + description = "Whether to create the default network for the project." + type = bool + default = false +} + +variable "billing_account" { + description = "Billing account id." + type = string + default = null +} + +variable "contacts" { + description = "List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES." + type = map(list(string)) + default = {} + nullable = false +} + +variable "custom_roles" { + description = "Map of role name => list of permissions to create in this project." + type = map(list(string)) + default = {} + nullable = false +} + +variable "descriptive_name" { + description = "Name of the project name. Used for project name instead of `name` variable." + type = string + default = null +} + +variable "group_iam" { + description = "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." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam" { + description = "IAM bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_additive" { + description = "IAM additive bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +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 "labels" { + description = "Resource labels." + type = map(string) + default = {} + nullable = false +} + +variable "lien_reason" { + description = "If non-empty, creates a project lien with this description." + type = string + default = "" +} + +variable "logging_exclusions" { + description = "Logging exclusions for this project in the form {NAME -> FILTER}." + type = map(string) + default = {} + nullable = false +} + +variable "logging_sinks" { + description = "Logging sinks to create for this project." + type = map(object({ + destination = string + type = string + filter = string + iam = bool + unique_writer = bool + # TODO exclusions also support description and disabled + exclusions = map(string) + })) + validation { + condition = alltrue([ + for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } + default = {} + nullable = false +} + +variable "metric_scopes" { + description = "List of projects that will act as metric scopes for this project." + type = list(string) + default = [] + nullable = false +} + +variable "name" { + description = "Project name and id suffix." + type = string +} + +variable "oslogin" { + description = "Enable OS Login." + type = bool + default = false +} + +variable "oslogin_admins" { + description = "List of IAM-style identities that will be granted roles necessary for OS Login administrators." + type = list(string) + default = [] + nullable = false + +} + +variable "oslogin_users" { + description = "List of IAM-style identities that will be granted roles necessary for OS Login users." + type = list(string) + default = [] + nullable = false +} + +variable "parent" { + description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "policy_boolean" { + description = "Map of boolean org policies and enforcement value, set value to null for policy restore." + type = map(bool) + default = {} + nullable = false +} + +variable "policy_list" { + description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." + type = map(object({ + inherit_from_parent = bool + suggested_value = string + status = bool + values = list(string) + })) + default = {} + nullable = false +} + +variable "prefix" { + description = "Prefix used to generate project id and name." + type = string + default = null +} + +variable "project_create" { + description = "Create project. When set to false, uses a data source to reference existing project." + type = bool + default = true +} + +variable "service_config" { + description = "Configure service API activation." + type = object({ + disable_on_destroy = bool + disable_dependent_services = bool + }) + default = { + disable_on_destroy = true + disable_dependent_services = true + } +} + +variable "service_encryption_key_ids" { + description = "Cloud KMS encryption key in {SERVICE => [KEY_URL]} format." + type = map(list(string)) + default = {} +} + +# accessPolicies/ACCESS_POLICY_NAME/servicePerimeters/PERIMETER_NAME +variable "service_perimeter_bridges" { + description = "Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format." + type = list(string) + default = null +} + +# accessPolicies/ACCESS_POLICY_NAME/servicePerimeters/PERIMETER_NAME +variable "service_perimeter_standard" { + description = "Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format." + type = string + default = null +} + +variable "services" { + description = "Service APIs to enable." + type = list(string) + default = [] +} + +variable "shared_vpc_host_config" { + description = "Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project)." + type = object({ + enabled = bool + service_projects = list(string) + }) + default = null +} + +variable "shared_vpc_service_config" { + description = "Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config)." + # the list of valid service identities is in service-accounts.tf + type = object({ + host_project = string + service_identity_iam = map(list(string)) + }) + default = null +} + +variable "skip_delete" { + description = "Allows the underlying resources to be destroyed without destroying the project itself." + type = bool + default = false +} + +variable "tag_bindings" { + description = "Tag bindings for this project, in key => tag value id format." + type = map(string) + default = null +} diff --git a/examples/guardrails/azuredevops/project-factory/modules/project/versions.tf b/examples/guardrails/azuredevops/project-factory/modules/project/versions.tf new file mode 100644 index 0000000..e72a780 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.1.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/azuredevops/project-factory/modules/project/vpc-sc.tf b/examples/guardrails/azuredevops/project-factory/modules/project/vpc-sc.tf new file mode 100644 index 0000000..edaa203 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project/vpc-sc.tf @@ -0,0 +1,45 @@ +/** + * 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 + * + * 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 VPC-SC project-level perimeter configuration. + +moved { + from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-standard + to = google_access_context_manager_service_perimeter_resource.standard +} + +resource "google_access_context_manager_service_perimeter_resource" "standard" { + count = var.service_perimeter_standard != null ? 1 : 0 + # this needs an additional lifecycle block in the vpc module on the + # google_access_context_manager_service_perimeter resource + perimeter_name = var.service_perimeter_standard + resource = "projects/${local.project.number}" +} + +moved { + from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-bridges + to = google_access_context_manager_service_perimeter_resource.bridge +} + +resource "google_access_context_manager_service_perimeter_resource" "bridge" { + for_each = toset( + var.service_perimeter_bridges != null ? var.service_perimeter_bridges : [] + ) + # this needs an additional lifecycle block in the vpc module on the + # google_access_context_manager_service_perimeter resource + perimeter_name = each.value + resource = "projects/${local.project.number}" +} diff --git a/examples/guardrails/azuredevops/project-factory/modules/project_plus/README.md b/examples/guardrails/azuredevops/project-factory/modules/project_plus/README.md new file mode 100644 index 0000000..2976a30 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project_plus/README.md @@ -0,0 +1 @@ +This is an addon for the project module with connects one service account to one project. \ No newline at end of file diff --git a/examples/guardrails/azuredevops/project-factory/modules/project_plus/main.tf b/examples/guardrails/azuredevops/project-factory/modules/project_plus/main.tf new file mode 100644 index 0000000..7330dd0 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project_plus/main.tf @@ -0,0 +1,46 @@ +/** + * 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 + * + * 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. + */ + +resource "random_id" "rand" { + byte_length = 4 +} + +module "project" { + source = "./../project" + name = "${var.team}-prj-${random_id.rand.hex}" + parent = var.folder + billing_account = var.billing_account +} + +resource "google_service_account" "sa" { + account_id = "${var.team}-sa-${random_id.rand.hex}" + display_name = "Service account ${var.team}" + project = module.project.project_id +} + +resource "google_service_account_iam_member" "sa-iam" { + service_account_id = google_service_account.sa.name + role = "roles/iam.workloadIdentityUser" + member = "principalSet://iam.googleapis.com/${var.wif-pool}/attribute.sub/${var.repo_sub}" +} + +resource "google_project_iam_member" "sa-project" { + for_each = toset(var.roles) + role = each.value + member = "serviceAccount:${google_service_account.sa.email}" + project = module.project.project_id + depends_on = [google_service_account.sa] +} diff --git a/examples/guardrails/azuredevops/project-factory/modules/project_plus/outputs.tf b/examples/guardrails/azuredevops/project-factory/modules/project_plus/outputs.tf new file mode 100644 index 0000000..c589d87 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project_plus/outputs.tf @@ -0,0 +1,36 @@ +/** + * 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 + * + * 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. + */ + +output "project_id" { + description = "Project ID" + value = module.project.project_id +} + +output "service_account_email" { + description = "Service Account Email" + value = google_service_account.sa.email +} + +output "repo_sub" { + description = "Repository" + value = var.repo_sub +} + +output "repo_provider" { + description = "Repository Provider" + value = var.repo_provider +} + diff --git a/examples/guardrails/azuredevops/project-factory/modules/project_plus/variables.tf b/examples/guardrails/azuredevops/project-factory/modules/project_plus/variables.tf new file mode 100644 index 0000000..67524ae --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project_plus/variables.tf @@ -0,0 +1,52 @@ +/** + * 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 + * + * 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. + */ + + +variable "team" { + description = "Team name." + type = string +} + +variable "repo_sub" { + description = "Repository path" + type = string +} + +variable "repo_provider" { + description = "Repository provider" + type = string +} + +variable "billing_account" { + description = "Billing account name." + type = string +} + +variable "folder" { + description = "Folder name." + type = string +} + +variable "roles" { + description = "Roles to attach." + type = list(string) + default = [] +} + +variable "wif-pool" { + description = "WIF pool name." + type = string +} diff --git a/examples/guardrails/azuredevops/project-factory/modules/project_plus/versions.tf b/examples/guardrails/azuredevops/project-factory/modules/project_plus/versions.tf new file mode 100644 index 0000000..2904126 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/modules/project_plus/versions.tf @@ -0,0 +1,29 @@ +# 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. + +terraform { + required_version = ">= 1.0.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +} + + diff --git a/examples/guardrails/azuredevops/project-factory/outputs.tf b/examples/guardrails/azuredevops/project-factory/outputs.tf new file mode 100644 index 0000000..43d9c4f --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/outputs.tf @@ -0,0 +1,40 @@ +/** + * 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 + * + * 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. + */ + +output "wif_pool_id_gitlab" { + description = "Gitlab Workload Identity Pool" + value = google_iam_workload_identity_pool.wif-pool-gitlab.name +} + +output "wif_provider_id_gitlab" { + description = "Gitlab Workload Identity Pool" + value = google_iam_workload_identity_pool_provider.wif-provider-gitlab.name +} + +output "wif_pool_id_github" { + description = "Github Workload Identity Pool" + value = google_iam_workload_identity_pool.wif-pool-github.name +} + +output "wif_provider_id_github" { + description = "Github Workload Identity Pool" + value = google_iam_workload_identity_pool_provider.wif-provider-github.name +} + +output "projects" { + description = "Created projects and service accounts." + value = module.project +} \ No newline at end of file diff --git a/examples/guardrails/azuredevops/project-factory/provider.tf b/examples/guardrails/azuredevops/project-factory/provider.tf new file mode 100644 index 0000000..21e2617 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/provider.tf @@ -0,0 +1,37 @@ +/** + * 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 + * + * 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. + */ + +terraform { + # backend "gcs" { + # bucket = "terraform-backend-bucket-azuredevops" + # prefix = "state" + # } + required_providers { + google = { + source = "hashicorp/google" + version = "~>4.11" + } + } +} + +provider "google" { + project = "azuretogcp-374609" + region = "us-central1" +} + +provider "google-beta" { + +} diff --git a/examples/guardrails/azuredevops/project-factory/variables.tf b/examples/guardrails/azuredevops/project-factory/variables.tf new file mode 100644 index 0000000..5a24e38 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/variables.tf @@ -0,0 +1,19 @@ +/** + * 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 + * + * 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. + */ + +variable "folder" { + default = "folders/71282294003" +} diff --git a/examples/guardrails/azuredevops/project-factory/wif.tf b/examples/guardrails/azuredevops/project-factory/wif.tf new file mode 100644 index 0000000..ae280ef --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/wif.tf @@ -0,0 +1,67 @@ +/** + * 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 + * + * 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. + */ + +resource "random_id" "rand" { + byte_length = 4 +} + +module "wif-project" { + source = "./modules/project" + name = "wif-prj-${random_id.rand.hex}" + parent = var.folder + billing_account = "01EDB8-9273DF-AA9623" +} + +resource "google_iam_workload_identity_pool" "wif-pool-gitlab" { + provider = google-beta + workload_identity_pool_id = "gitlab-pool-${random_id.rand.hex}" + project = module.wif-project.project_id +} + +resource "google_iam_workload_identity_pool_provider" "wif-provider-gitlab" { + provider = google-beta + workload_identity_pool_id = google_iam_workload_identity_pool.wif-pool-gitlab.workload_identity_pool_id + workload_identity_pool_provider_id = "gitlab-provider-${random_id.rand.hex}" + project = module.wif-project.project_id + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.sub" = "assertion.sub" + } + oidc { + issuer_uri = "https://gitlab.com" + } +} + +resource "google_iam_workload_identity_pool" "wif-pool-github" { + provider = google-beta + workload_identity_pool_id = "github-pool-${random_id.rand.hex}" + project = module.wif-project.project_id +} + +resource "google_iam_workload_identity_pool_provider" "wif-provider-github" { + provider = google-beta + workload_identity_pool_id = google_iam_workload_identity_pool.wif-pool-github.workload_identity_pool_id + workload_identity_pool_provider_id = "github-provider-${random_id.rand.hex}" + project = module.wif-project.project_id + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.sub" = "assertion.sub" + "attribute.actor" = "assertion.actor" + } + oidc { + issuer_uri = "https://token.actions.githubusercontent.com" + } +} diff --git a/examples/guardrails/azuredevops/skunkworks/.github/workflows/tf-actions-dev.yml b/examples/guardrails/azuredevops/skunkworks/.github/workflows/tf-actions-dev.yml new file mode 100644 index 0000000..2fbf4c1 --- /dev/null +++ b/examples/guardrails/azuredevops/skunkworks/.github/workflows/tf-actions-dev.yml @@ -0,0 +1,91 @@ +# 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 +# +# 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. + +name: 'DEV Deployment' + +on: + push: + branches: + - dev + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.DEV_SERVICE_ACCOUNT }} + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve + + diff --git a/examples/guardrails/azuredevops/skunkworks/.github/workflows/tf-actions-prod.yml b/examples/guardrails/azuredevops/skunkworks/.github/workflows/tf-actions-prod.yml new file mode 100644 index 0000000..4159feb --- /dev/null +++ b/examples/guardrails/azuredevops/skunkworks/.github/workflows/tf-actions-prod.yml @@ -0,0 +1,91 @@ +# 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 +# +# 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. + +name: 'PROD Deployment' + +on: + push: + branches: + - main + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.PROD_SERVICE_ACCOUNT }} + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve + + diff --git a/examples/guardrails/azuredevops/skunkworks/.github/workflows/tf-actions-stage.yml b/examples/guardrails/azuredevops/skunkworks/.github/workflows/tf-actions-stage.yml new file mode 100644 index 0000000..0f20fa0 --- /dev/null +++ b/examples/guardrails/azuredevops/skunkworks/.github/workflows/tf-actions-stage.yml @@ -0,0 +1,91 @@ +# 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 +# +# 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. + +name: 'STAGE Deployment' + +on: + push: + branches: + - stage + +env: + STATE_BUCKET: ${{ secrets.STATE_BUCKET }} + WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ secrets.STAGE_SERVICE_ACCOUNT }} + +# OR SET MANUALLY +#env: +# STATE_BUCKET: 'XXXX' +# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' +# SERVICE_ACCOUNT: 'XXXX@XXXX' + +jobs: + + terraform: + name: 'terraform' + runs-on: ubuntu-latest + environment: production + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest + defaults: + run: + shell: bash + + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + token_format: 'access_token' + WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} + access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) + + # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} + + # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. + - name: Terraform Init + run: | + terraform init \ + -backend-config="bucket=$STATE_BUCKET" \ + -backend-config="prefix=$GITHUB_REPOSITORY" \ + + # Checks that all Terraform configuration files adhere to a canonical format + #- name: Terraform Format + # run: terraform fmt -check + + # Generates an execution plan for Terraform + - name: Terraform Plan + run: terraform plan + + # On push to main, build or change infrastructure according to Terraform configuration files + # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve + + diff --git a/examples/guardrails/azuredevops/skunkworks/README.md b/examples/guardrails/azuredevops/skunkworks/README.md new file mode 100644 index 0000000..0b89bb2 --- /dev/null +++ b/examples/guardrails/azuredevops/skunkworks/README.md @@ -0,0 +1,29 @@ +# Skunkworks - IaC Kickstarter Template + +This is a template for an IaC kickstarter repository. + +![Skunkworks](https://user-images.githubusercontent.com/94000358/169810982-36f01de2-e5e5-4ecd-b98e-3cf5a6aa9f81.png) + +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. + +This template creates a bucket in the specified target environment. + +## Repository Configuration +This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: + +Secret configuration + +or by modifing the [Workflow Action Files](.github/workflows/) and setting the environment variables: +``` +env: + STATE_BUCKET: 'XXXX' + # The GCS bucket to store the terraform state + WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' + # The workload identity provider that should be used for this repository. + SERVICE_ACCOUNT: 'XXXX@XXXX' + # The service account that should be used for this repository. +``` diff --git a/examples/guardrails/azuredevops/skunkworks/azure-pipeline.yml b/examples/guardrails/azuredevops/skunkworks/azure-pipeline.yml new file mode 100644 index 0000000..814a166 --- /dev/null +++ b/examples/guardrails/azuredevops/skunkworks/azure-pipeline.yml @@ -0,0 +1,124 @@ +name: $(Date:yyyyMMdd)$(Rev:.r) + +trigger: + - none +pool: + vmImage: ubuntu-latest +stages: + - stage: auth + displayName: "GCP WIF Auth" + jobs: + - job: governance_pipeline + timeoutInMinutes: 20 + steps: + - task: charleszipp.azure-pipelines-tasks-terraform.azure-pipelines-tasks-terraform-installer.TerraformInstaller@0 + displayName: install terraform + inputs: + terraformVersion: latest + + # AzureCLI task to retrieve an Azure token for the Service Principal and then exchanges it against a Service Account token using Workload Identity Federation + + - task: AzureCLI@2 + displayName: get access token + inputs: + azureSubscription: 'azuretogcp' + scriptType: 'bash' + scriptLocation: 'inlineScript' + inlineScript: | + SUBJECT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt" + SUBJECT_TOKEN=$(az account get-access-token --query accessToken --output tsv) + STS_TOKEN=$(curl --silent -0 -X POST https://sts.googleapis.com/v1/token \ + -H 'Content-Type: text/json; charset=utf-8' \ + -d @- < Date: Tue, 28 Feb 2023 14:15:46 -0600 Subject: [PATCH 014/132] Create README.md --- examples/guardrails/gitlab/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 examples/guardrails/gitlab/README.md diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md new file mode 100644 index 0000000..77e3f22 --- /dev/null +++ b/examples/guardrails/gitlab/README.md @@ -0,0 +1,16 @@ +This section covers CICD process using Gitlab and deploying resources to GCP. + +Please look through the [README](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/README.md) for an overview of CICD process. + + +## How to Run this stage +* Create a Group in gitlab +* Add project called “folder factory” and copy code from devops folder factory repo into it +* Add project called “project factory” and copy code from devops project factory repo into it +* Add a project called “skunkworks” and copy code from devops skunkworks repo into it. +* Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. + + +* For further details on workload identity federation setup for Gitlab with GCP, please refer to the official Gitlab Documentation +* Available self hosted or SaaS Gitlab runners to run the pipelines. + From da36d6630d13f00c615126f6d00decdc6b0e790e Mon Sep 17 00:00:00 2001 From: agutta Date: Tue, 28 Feb 2023 14:17:48 -0600 Subject: [PATCH 015/132] Update README.md --- examples/guardrails/gitlab/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index 77e3f22..dc4cfa5 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -5,8 +5,8 @@ Please look through the [README](https://github.com/google/devops-governance/blo ## How to Run this stage * Create a Group in gitlab -* Add project called “folder factory” and copy code from devops folder factory repo into it -* Add project called “project factory” and copy code from devops project factory repo into it +* Add a project called “folder-factory” and copy code from devops [folder factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/folder-factory) into it +* Add project called “project-factory” and copy code from devops project factory repo into it * Add a project called “skunkworks” and copy code from devops skunkworks repo into it. * Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. From dd02c04b150c05f5c2379ac89d61dd90e50f06a8 Mon Sep 17 00:00:00 2001 From: agutta Date: Tue, 28 Feb 2023 14:18:41 -0600 Subject: [PATCH 016/132] Update README.md --- examples/guardrails/gitlab/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index dc4cfa5..80ce5b0 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -6,7 +6,7 @@ Please look through the [README](https://github.com/google/devops-governance/blo ## How to Run this stage * Create a Group in gitlab * Add a project called “folder-factory” and copy code from devops [folder factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/folder-factory) into it -* Add project called “project-factory” and copy code from devops project factory repo into it +* Add project called “project-factory” and copy code from devops [project factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/project-factory) into it * Add a project called “skunkworks” and copy code from devops skunkworks repo into it. * Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. From 29a0e1c1f272ae936e986501bc83f4279a3f957a Mon Sep 17 00:00:00 2001 From: agutta Date: Tue, 28 Feb 2023 14:19:00 -0600 Subject: [PATCH 017/132] Update README.md --- examples/guardrails/gitlab/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index 80ce5b0..fd34842 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -7,7 +7,7 @@ Please look through the [README](https://github.com/google/devops-governance/blo * Create a Group in gitlab * Add a project called “folder-factory” and copy code from devops [folder factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/folder-factory) into it * Add project called “project-factory” and copy code from devops [project factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/project-factory) into it -* Add a project called “skunkworks” and copy code from devops skunkworks repo into it. +* Add a project called “skunkworks” and copy code from devops [skunkworks repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/skunkworks) into it. * Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. From a8d31126e7e1088254c2376f116fef667d774f0d Mon Sep 17 00:00:00 2001 From: agutta Date: Tue, 28 Feb 2023 14:19:46 -0600 Subject: [PATCH 018/132] Update README.md --- examples/guardrails/gitlab/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index fd34842..7c50662 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -4,6 +4,7 @@ Please look through the [README](https://github.com/google/devops-governance/blo ## How to Run this stage +* Create a gitlab account * Create a Group in gitlab * Add a project called “folder-factory” and copy code from devops [folder factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/folder-factory) into it * Add project called “project-factory” and copy code from devops [project factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/project-factory) into it From 66139dfc5ad997aa7cfab5678972bcfffc1f7a3c Mon Sep 17 00:00:00 2001 From: agutta Date: Tue, 28 Feb 2023 14:26:02 -0600 Subject: [PATCH 019/132] Update README.md --- examples/guardrails/gitlab/README.md | 40 +++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index 7c50662..59fff58 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -1,4 +1,4 @@ -This section covers CICD process using Gitlab and deploying resources to GCP. +This workflow runs terraform pipelines using Gitlab CICD. This document covers the steps to setup Gitlab CICD and provides a high level overview of the stages involved. Please look through the [README](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/README.md) for an overview of CICD process. @@ -15,3 +15,41 @@ Please look through the [README](https://github.com/google/devops-governance/blo * For further details on workload identity federation setup for Gitlab with GCP, please refer to the official Gitlab Documentation * Available self hosted or SaaS Gitlab runners to run the pipelines. + + +Prerequisites +A Gitlab project containing the folder-factory repository +Available Gitlab Runners for the project either self hosted or the SaaS Gitlab shared runners. +A working WIF provider, pool setup with audience as gitlab.com with neccessary attributes and linked service account with required permissions. For further details, please refer to the official Gitlab Documentation +Setup +Update the CICD configuration file path in the repository + +Terraform pipeline execution with Gitlab runners and Gitlab repository + +From the folder-factory Gitlab project page, Navigate to Settings > CICD > expand General pipelines +update CI/CD configuration file value to the relative path of the gitlab-ci.yml file from the root directory +Update the CI/CD variables + +From the folder-factory project page, Navigate to Settings > CICD > expand Variables +Add the below variables to the pipeline +Variable Description Sample value +GCP_PROJECT_ID The GCP project ID of your service account sample-project-1122 +GCP_SERVICE_ACCOUNT The Service Account to be used for creating folders xyz@sample-project-1122.iam.gserviceaccount.com +GCP_WORKLOAD_IDENTITY_PROVIDER The Workload Identity provider URI configured with the Service Account and the repository projects//locations/global/workloadIdentityPools//providers/ +STATE_BUCKET The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket sample-terraform-state-bucket +TF_LOG The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity WARN +TF_ROOT The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables $CI_PROJECT_DIR +TF_VERSION The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. 1.3.6 +Overview of the Pipeline stages +The complete workflow consists of 4 stages and 2 before-script jobs + +before_script jobs : + +gcp-auth : creates the wif credentials by impersonating the service account. +terraform init : initializes terraform in the specified TF_ROOT directory +Stages: + +setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages +validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command +plan: Runs terraform plan and saves the plan and json version of the plan as artifacts +apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once plan is successful. Runs terraform apply and creates the infrastructure specified. From 0ab16277cd06d2fb8dd82d8688cc8ea6f84fb7f0 Mon Sep 17 00:00:00 2001 From: agutta Date: Tue, 28 Feb 2023 14:30:29 -0600 Subject: [PATCH 020/132] Update README.md --- examples/guardrails/gitlab/README.md | 65 +++++++++++++++------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index 59fff58..a6c6530 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -4,52 +4,55 @@ Please look through the [README](https://github.com/google/devops-governance/blo ## How to Run this stage + +## Prerequisites * Create a gitlab account * Create a Group in gitlab + +## Gitlab Project Setup * Add a project called “folder-factory” and copy code from devops [folder factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/folder-factory) into it * Add project called “project-factory” and copy code from devops [project factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/project-factory) into it * Add a project called “skunkworks” and copy code from devops [skunkworks repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/skunkworks) into it. -* Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. +## Workload Identity setup +* Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. +* A working WIF provider, pool setup with audience as gitlab.com with neccessary attributes and linked service account with required permissions. For further details, please refer to the official [Gitlab Documentation](https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/) * For further details on workload identity federation setup for Gitlab with GCP, please refer to the official Gitlab Documentation * Available self hosted or SaaS Gitlab runners to run the pipelines. -Prerequisites -A Gitlab project containing the folder-factory repository -Available Gitlab Runners for the project either self hosted or the SaaS Gitlab shared runners. -A working WIF provider, pool setup with audience as gitlab.com with neccessary attributes and linked service account with required permissions. For further details, please refer to the official Gitlab Documentation -Setup -Update the CICD configuration file path in the repository +# Terraform pipeline execution with Gitlab runners and Gitlab repository -Terraform pipeline execution with Gitlab runners and Gitlab repository +1. Update the CICD configuration file path in the repository + * From the folder-factory Gitlab project page, Navigate to Settings > CICD > expand General pipelines + * update CI/CD configuration file value to the relative path of the gitlab-ci.yml file from the root directory + * sample path (.gitlab/workflows/.gitlab-ci.yml) -From the folder-factory Gitlab project page, Navigate to Settings > CICD > expand General pipelines -update CI/CD configuration file value to the relative path of the gitlab-ci.yml file from the root directory -Update the CI/CD variables +2. Update the CI/CD variables + * From the folder-factory project page, Navigate to Settings > CICD > expand Variables + * Add the below variables to the pipeline -From the folder-factory project page, Navigate to Settings > CICD > expand Variables -Add the below variables to the pipeline -Variable Description Sample value -GCP_PROJECT_ID The GCP project ID of your service account sample-project-1122 -GCP_SERVICE_ACCOUNT The Service Account to be used for creating folders xyz@sample-project-1122.iam.gserviceaccount.com -GCP_WORKLOAD_IDENTITY_PROVIDER The Workload Identity provider URI configured with the Service Account and the repository projects//locations/global/workloadIdentityPools//providers/ -STATE_BUCKET The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket sample-terraform-state-bucket -TF_LOG The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity WARN -TF_ROOT The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables $CI_PROJECT_DIR -TF_VERSION The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. 1.3.6 -Overview of the Pipeline stages -The complete workflow consists of 4 stages and 2 before-script jobs +| Variable | Description | Sample value | +|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| GCP_PROJECT_ID | The GCP project ID of your service account | sample-project-1122 | +| GCP_SERVICE_ACCOUNT | The Service Account to be used for creating folders | xyz@sample-project-1122.iam.gserviceaccount.com | +| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects//locations/global/workloadIdentityPools//providers/ | +| STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | +| TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | +| TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | +| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | -before_script jobs : +## Overview of the Pipeline stages +The complete workflow consists of 4 stages and 2 before-script jobs -gcp-auth : creates the wif credentials by impersonating the service account. -terraform init : initializes terraform in the specified TF_ROOT directory -Stages: +* before_script jobs : + * gcp-auth : creates the wif credentials by impersonating the service account. + * terraform init : initializes terraform in the specified TF_ROOT directory -setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages -validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command -plan: Runs terraform plan and saves the plan and json version of the plan as artifacts -apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once plan is successful. Runs terraform apply and creates the infrastructure specified. +* Stages: + * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages + * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command + * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts + * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once plan is successful. Runs terraform apply and creates the infrastructure specified. From 29ca1155a1c1938148483fe5e232f6a12ea65b92 Mon Sep 17 00:00:00 2001 From: agutta Date: Tue, 28 Feb 2023 14:42:19 -0600 Subject: [PATCH 021/132] Update README.md --- examples/guardrails/gitlab/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index a6c6530..4a4ff84 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -23,7 +23,7 @@ Please look through the [README](https://github.com/google/devops-governance/blo -# Terraform pipeline execution with Gitlab runners and Gitlab repository +# Terraform pipeline setup 1. Update the CICD configuration file path in the repository * From the folder-factory Gitlab project page, Navigate to Settings > CICD > expand General pipelines @@ -44,6 +44,12 @@ Please look through the [README](https://github.com/google/devops-governance/blo | TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | | TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | +## Terraform pipeline setup +* folder-factoy looks for folder specifications in yaml format in data/folders +* There is a sample file provided. Remove "folder.yaml.sample" to ".yaml" +* merge the code into main and pipeline should start. +* Look in troubleshooting section if there are any issues. + ## Overview of the Pipeline stages The complete workflow consists of 4 stages and 2 before-script jobs From f8d0da23fde4a8a12540374c89cca8ab11dd642d Mon Sep 17 00:00:00 2001 From: agutta Date: Tue, 28 Feb 2023 15:25:20 -0600 Subject: [PATCH 022/132] Update README.md --- examples/guardrails/gitlab/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index 4a4ff84..d08120c 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -62,3 +62,15 @@ The complete workflow consists of 4 stages and 2 before-script jobs * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once plan is successful. Runs terraform apply and creates the infrastructure specified. + +## Troubleshooting +* Gitlab documentation for provider creation gives “attribute.X” and “assertion.X” incase using those make sure they are in lowercase. +* Issuer URL need to have “/” at the end https://gitlab.com/ +* “Cannot create a pool with the same name” - Requested entity already exists. How long can we not create it? +* "Permission \'iam.serviceAccounts.getAccessToken\' denied on resource (or it may not exist). + + + + +is there any correlation between what we specify as and the principal set we provide permissions for + From 939fcb535d4e975b76e5f5fdb70b9758c6b5325d Mon Sep 17 00:00:00 2001 From: Anita Gutta Date: Tue, 28 Feb 2023 15:34:59 -0600 Subject: [PATCH 023/132] Running terraform fmt --- .../guardrails/gitlab/folder-factory/main.tf | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/guardrails/gitlab/folder-factory/main.tf b/examples/guardrails/gitlab/folder-factory/main.tf index d024c17..f21d44e 100644 --- a/examples/guardrails/gitlab/folder-factory/main.tf +++ b/examples/guardrails/gitlab/folder-factory/main.tf @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - + locals { folders = { for f in fileset("./data/folders", "**/*.yaml") : @@ -22,11 +22,11 @@ locals { } module "folder" { - source = "./modules/folder" - for_each = local.folders - name = each.key - parent = each.value.parent - policy_boolean = try(each.value.org_policies.policy_boolean, {}) - policy_list = try(each.value.org_policies.policy_list, {}) - iam = try(each.value.iam, {}) + source = "./modules/folder" + for_each = local.folders + name = each.key + parent = each.value.parent + policy_boolean = try(each.value.org_policies.policy_boolean, {}) + policy_list = try(each.value.org_policies.policy_list, {}) + iam = try(each.value.iam, {}) } \ No newline at end of file From e09065fa874b975f611cb712c433de8744d11c7a Mon Sep 17 00:00:00 2001 From: Anita Gutta Date: Tue, 28 Feb 2023 15:37:15 -0600 Subject: [PATCH 024/132] Running terraform fmt --- examples/guardrails/gitlab/project-factory/main.tf | 4 ++-- .../project-factory/modules/project_plus/main.tf | 8 ++++---- .../guardrails/gitlab/project-factory/provider.tf | 2 +- .../guardrails/gitlab/project-factory/variables.tf | 2 +- examples/guardrails/gitlab/project-factory/wif.tf | 12 ++++++------ 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/guardrails/gitlab/project-factory/main.tf b/examples/guardrails/gitlab/project-factory/main.tf index 1e89f2b..56414b9 100644 --- a/examples/guardrails/gitlab/project-factory/main.tf +++ b/examples/guardrails/gitlab/project-factory/main.tf @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - + locals { projects = { for f in fileset("./data/projects", "**/*.yaml") : @@ -31,5 +31,5 @@ module "project" { folder = var.folder roles = try(each.value.roles, []) wif-pool = each.value.repo_provider == "gitlab" ? google_iam_workload_identity_pool.wif-pool-gitlab.name : google_iam_workload_identity_pool.wif-pool-github.name - depends_on = [google_iam_workload_identity_pool.wif-pool-github,google_iam_workload_identity_pool.wif-pool-gitlab] + depends_on = [google_iam_workload_identity_pool.wif-pool-github, google_iam_workload_identity_pool.wif-pool-gitlab] } diff --git a/examples/guardrails/gitlab/project-factory/modules/project_plus/main.tf b/examples/guardrails/gitlab/project-factory/modules/project_plus/main.tf index 7330dd0..6e51671 100644 --- a/examples/guardrails/gitlab/project-factory/modules/project_plus/main.tf +++ b/examples/guardrails/gitlab/project-factory/modules/project_plus/main.tf @@ -38,9 +38,9 @@ resource "google_service_account_iam_member" "sa-iam" { } resource "google_project_iam_member" "sa-project" { - for_each = toset(var.roles) - role = each.value - member = "serviceAccount:${google_service_account.sa.email}" - project = module.project.project_id + for_each = toset(var.roles) + role = each.value + member = "serviceAccount:${google_service_account.sa.email}" + project = module.project.project_id depends_on = [google_service_account.sa] } diff --git a/examples/guardrails/gitlab/project-factory/provider.tf b/examples/guardrails/gitlab/project-factory/provider.tf index 34beb2f..0e17ef9 100644 --- a/examples/guardrails/gitlab/project-factory/provider.tf +++ b/examples/guardrails/gitlab/project-factory/provider.tf @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - + terraform { backend "gcs" { } diff --git a/examples/guardrails/gitlab/project-factory/variables.tf b/examples/guardrails/gitlab/project-factory/variables.tf index df6e79d..88d1db0 100644 --- a/examples/guardrails/gitlab/project-factory/variables.tf +++ b/examples/guardrails/gitlab/project-factory/variables.tf @@ -15,5 +15,5 @@ */ variable "folder" { - + } diff --git a/examples/guardrails/gitlab/project-factory/wif.tf b/examples/guardrails/gitlab/project-factory/wif.tf index 7201ae9..faec2e7 100644 --- a/examples/guardrails/gitlab/project-factory/wif.tf +++ b/examples/guardrails/gitlab/project-factory/wif.tf @@ -36,9 +36,9 @@ resource "google_iam_workload_identity_pool_provider" "wif-provider-gitlab" { workload_identity_pool_id = google_iam_workload_identity_pool.wif-pool-gitlab.workload_identity_pool_id workload_identity_pool_provider_id = "gitlab-provider-${random_id.rand.hex}" project = module.wif-project.project_id - attribute_mapping = { + attribute_mapping = { "google.subject" = "assertion.sub" - "attribute.sub" = "assertion.sub" + "attribute.sub" = "assertion.sub" } oidc { issuer_uri = "https://gitlab.com/" @@ -57,12 +57,12 @@ resource "google_iam_workload_identity_pool_provider" "wif-provider-github" { workload_identity_pool_id = google_iam_workload_identity_pool.wif-pool-github.workload_identity_pool_id workload_identity_pool_provider_id = "github-provider-${random_id.rand.hex}" project = module.wif-project.project_id - attribute_mapping = { - "google.subject" = "assertion.sub" - "attribute.sub" = "assertion.sub" + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.sub" = "assertion.sub" "attribute.actor" = "assertion.actor" } oidc { - issuer_uri = "https://token.actions.githubusercontent.com" + issuer_uri = "https://token.actions.githubusercontent.com" } } From 1fc4e4e9533d470647cb0aea4de33e8738a4b823 Mon Sep 17 00:00:00 2001 From: Anita Gutta Date: Tue, 28 Feb 2023 15:37:55 -0600 Subject: [PATCH 025/132] Running terraform fmt --- examples/guardrails/gitlab/skunkworks/main.tf | 2 +- examples/guardrails/gitlab/skunkworks/variables.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/gitlab/skunkworks/main.tf b/examples/guardrails/gitlab/skunkworks/main.tf index 03926bd..cfc6436 100644 --- a/examples/guardrails/gitlab/skunkworks/main.tf +++ b/examples/guardrails/gitlab/skunkworks/main.tf @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - + resource "google_storage_bucket" "bucket" { project = var.project name = lower("${var.project}-test-bucket") diff --git a/examples/guardrails/gitlab/skunkworks/variables.tf b/examples/guardrails/gitlab/skunkworks/variables.tf index 6af98ff..ab4546b 100644 --- a/examples/guardrails/gitlab/skunkworks/variables.tf +++ b/examples/guardrails/gitlab/skunkworks/variables.tf @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - + variable "project" { type = string default = "project-id" From 56aeef9cf6645261380fa7da36ad246dac061aa1 Mon Sep 17 00:00:00 2001 From: agutta Date: Tue, 28 Feb 2023 15:54:53 -0600 Subject: [PATCH 026/132] Update README.md --- examples/guardrails/gitlab/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index d08120c..b8297e2 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -45,11 +45,21 @@ Please look through the [README](https://github.com/google/devops-governance/blo | TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | ## Terraform pipeline setup + +## Folder Factory * folder-factoy looks for folder specifications in yaml format in data/folders * There is a sample file provided. Remove "folder.yaml.sample" to ".yaml" * merge the code into main and pipeline should start. * Look in troubleshooting section if there are any issues. + +## Project Factory +* project-factoy looks for project specifications in yaml format in data/projects +* There is a sample file provided. Remove "project.yaml.sample" to ".yaml" +* update billing_account_id +* merge the code into main and pipeline should start. +* Look in troubleshooting section if there are any issues. + ## Overview of the Pipeline stages The complete workflow consists of 4 stages and 2 before-script jobs @@ -63,6 +73,7 @@ The complete workflow consists of 4 stages and 2 before-script jobs * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once plan is successful. Runs terraform apply and creates the infrastructure specified. + ## Troubleshooting * Gitlab documentation for provider creation gives “attribute.X” and “assertion.X” incase using those make sure they are in lowercase. * Issuer URL need to have “/” at the end https://gitlab.com/ From c0938a66a157b45ede83012561a2cf49cc0a3dc9 Mon Sep 17 00:00:00 2001 From: agutta Date: Tue, 28 Feb 2023 16:08:38 -0600 Subject: [PATCH 027/132] Update wif.tf --- examples/guardrails/gitlab/project-factory/wif.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/project-factory/wif.tf b/examples/guardrails/gitlab/project-factory/wif.tf index faec2e7..ecd842d 100644 --- a/examples/guardrails/gitlab/project-factory/wif.tf +++ b/examples/guardrails/gitlab/project-factory/wif.tf @@ -22,7 +22,7 @@ module "wif-project" { source = "./modules/project" name = "wif-prj-${random_id.rand.hex}" parent = var.folder - billing_account = "01B3B2-962224-4EEC67" + billing_account = var.billing_account } resource "google_iam_workload_identity_pool" "wif-pool-gitlab" { From 67e674eeb8062c68195b47ad3034dfa0e349e624 Mon Sep 17 00:00:00 2001 From: agutta Date: Tue, 28 Feb 2023 16:09:09 -0600 Subject: [PATCH 028/132] Update variables.tf --- examples/guardrails/gitlab/project-factory/variables.tf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/guardrails/gitlab/project-factory/variables.tf b/examples/guardrails/gitlab/project-factory/variables.tf index 88d1db0..c9805dc 100644 --- a/examples/guardrails/gitlab/project-factory/variables.tf +++ b/examples/guardrails/gitlab/project-factory/variables.tf @@ -17,3 +17,8 @@ variable "folder" { } + +variable "billing_account" { + type = string + description = "GCP Billing Account" +} From fe889434726e3f673c65db827db7d2ece227797b Mon Sep 17 00:00:00 2001 From: agutta Date: Tue, 28 Feb 2023 16:09:32 -0600 Subject: [PATCH 029/132] Create terraform.tfvars.sample --- .../guardrails/gitlab/project-factory/terraform.tfvars.sample | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 examples/guardrails/gitlab/project-factory/terraform.tfvars.sample diff --git a/examples/guardrails/gitlab/project-factory/terraform.tfvars.sample b/examples/guardrails/gitlab/project-factory/terraform.tfvars.sample new file mode 100644 index 0000000..a198a51 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/terraform.tfvars.sample @@ -0,0 +1,2 @@ +folder = "folders/" +billing_account = "" From c75cec2862d5c97fb97f75407b11d23d1da91adb Mon Sep 17 00:00:00 2001 From: agutta Date: Tue, 28 Feb 2023 18:24:30 -0600 Subject: [PATCH 030/132] Update README.md --- examples/guardrails/gitlab/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index b8297e2..17ca57d 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -6,6 +6,8 @@ Please look through the [README](https://github.com/google/devops-governance/blo ## How to Run this stage ## Prerequisites +* Create a service account? where and what permissions +* Create a storage bucket? where and what permissions are required? * Create a gitlab account * Create a Group in gitlab @@ -57,7 +59,8 @@ Please look through the [README](https://github.com/google/devops-governance/blo * project-factoy looks for project specifications in yaml format in data/projects * There is a sample file provided. Remove "project.yaml.sample" to ".yaml" * update billing_account_id -* merge the code into main and pipeline should start. +* Service Account needs Billing Account User permission" to create the project. At what level should we give the permission +* Merge the code into main and pipeline should start. * Look in troubleshooting section if there are any issues. ## Overview of the Pipeline stages From 76837e957d84833b8acbeb99b8f9d2dcb1c625bf Mon Sep 17 00:00:00 2001 From: agutta Date: Wed, 1 Mar 2023 07:05:49 -0600 Subject: [PATCH 031/132] Update README.md --- examples/guardrails/gitlab/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index 17ca57d..b90407f 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -1,6 +1,7 @@ This workflow runs terraform pipelines using Gitlab CICD. This document covers the steps to setup Gitlab CICD and provides a high level overview of the stages involved. -Please look through the [README](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/README.md) for an overview of CICD process. +This document covers the steps to setup gitlab CICD pipeline for terraform with gitlab SaaS shared runners.The setup involves setting up gitlab repository and the corresponding CI/CD settings and variables. The pipeline triggers based on select events (like push to specific branches), authenticates to the specified service account using Workload Identity federation and runs the pipeline to deploy infrastructure using terraform in GCP. Please look through the [README](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/README.md) for an overview of the CICD process. + ## How to Run this stage From 3c863e4cae79ec1782d003b31eee00fbf8b05e4e Mon Sep 17 00:00:00 2001 From: agutta Date: Wed, 1 Mar 2023 10:53:15 -0600 Subject: [PATCH 032/132] Update README.md --- examples/guardrails/gitlab/README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index b90407f..b9d9925 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -1,8 +1,13 @@ -This workflow runs terraform pipelines using Gitlab CICD. This document covers the steps to setup Gitlab CICD and provides a high level overview of the stages involved. - -This document covers the steps to setup gitlab CICD pipeline for terraform with gitlab SaaS shared runners.The setup involves setting up gitlab repository and the corresponding CI/CD settings and variables. The pipeline triggers based on select events (like push to specific branches), authenticates to the specified service account using Workload Identity federation and runs the pipeline to deploy infrastructure using terraform in GCP. Please look through the [README](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/README.md) for an overview of the CICD process. +This workflow covers the steps to setup gitlab CICD pipeline for terraform with gitlab SaaS shared runners. +The setup involves setting up gitlab repository and the corresponding CI/CD settings and variables. +The pipeline triggers based on select events (like push to specific branches), authenticates to the specified service account using Workload Identity federation and runs the pipeline to deploy infrastructure using terraform in GCP. +Please look through the [README](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/README.md) for an overview of the CICD process. +## High level Architecture +

+ Stages diagram +

## How to Run this stage From 9182c9d8a268f6274f4523f8205a56317ef56c8c Mon Sep 17 00:00:00 2001 From: agutta Date: Wed, 1 Mar 2023 10:59:02 -0600 Subject: [PATCH 033/132] Update README.md --- examples/guardrails/gitlab/README.md | 87 ++++------------------------ 1 file changed, 11 insertions(+), 76 deletions(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index b9d9925..8a65b9f 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -9,88 +9,23 @@ Please look through the [README](https://github.com/google/devops-governance/blo Stages diagram

-## How to Run this stage +## Implementation Process -## Prerequisites -* Create a service account? where and what permissions -* Create a storage bucket? where and what permissions are required? -* Create a gitlab account -* Create a Group in gitlab - -## Gitlab Project Setup -* Add a project called “folder-factory” and copy code from devops [folder factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/folder-factory) into it -* Add project called “project-factory” and copy code from devops [project factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/project-factory) into it -* Add a project called “skunkworks” and copy code from devops [skunkworks repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/skunkworks) into it. - -## Workload Identity setup -* Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. -* A working WIF provider, pool setup with audience as gitlab.com with neccessary attributes and linked service account with required permissions. For further details, please refer to the official [Gitlab Documentation](https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/) - -* For further details on workload identity federation setup for Gitlab with GCP, please refer to the official Gitlab Documentation -* Available self hosted or SaaS Gitlab runners to run the pipelines. - - - -# Terraform pipeline setup - -1. Update the CICD configuration file path in the repository - * From the folder-factory Gitlab project page, Navigate to Settings > CICD > expand General pipelines - * update CI/CD configuration file value to the relative path of the gitlab-ci.yml file from the root directory - * sample path (.gitlab/workflows/.gitlab-ci.yml) - -2. Update the CI/CD variables - * From the folder-factory project page, Navigate to Settings > CICD > expand Variables - * Add the below variables to the pipeline +The setup consists of configuring folder-factory, project-factory and skunkworks as three gitlab repositories. The pre-requisites and the setup are detailed below for each of the repositories. -| Variable | Description | Sample value | -|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| -| GCP_PROJECT_ID | The GCP project ID of your service account | sample-project-1122 | -| GCP_SERVICE_ACCOUNT | The Service Account to be used for creating folders | xyz@sample-project-1122.iam.gserviceaccount.com | -| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects//locations/global/workloadIdentityPools//providers/ | -| STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | -| TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | -| TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | -| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | +Workload Identity federation is a keyless authentication mechanism that is an important component of the CICD setup as it securely allows us to authenticate gitlab on GCP. It connects a defined Gitlab repository with a target service account and project within GCP for IaC. -## Terraform pipeline setup +The actual implementation for granting impersonation access on the service account to the WIF provider identity depends on your desired configuration. You can choose to let the service account be impersonated only from changes by a specific gitlab user or a specific gitlab project or a combination of project and a branch..etc. For further details on workload identity federation setup for Gitlab with GCP, please refer to the official [Gitlab Documentation](https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/) -## Folder Factory -* folder-factoy looks for folder specifications in yaml format in data/folders -* There is a sample file provided. Remove "folder.yaml.sample" to ".yaml" -* merge the code into main and pipeline should start. -* Look in troubleshooting section if there are any issues. - - -## Project Factory -* project-factoy looks for project specifications in yaml format in data/projects -* There is a sample file provided. Remove "project.yaml.sample" to ".yaml" -* update billing_account_id -* Service Account needs Billing Account User permission" to create the project. At what level should we give the permission -* Merge the code into main and pipeline should start. -* Look in troubleshooting section if there are any issues. - -## Overview of the Pipeline stages -The complete workflow consists of 4 stages and 2 before-script jobs - -* before_script jobs : - * gcp-auth : creates the wif credentials by impersonating the service account. - * terraform init : initializes terraform in the specified TF_ROOT directory - -* Stages: - * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages - * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command - * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts - * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once plan is successful. Runs terraform apply and creates the infrastructure specified. - - -## Troubleshooting -* Gitlab documentation for provider creation gives “attribute.X” and “assertion.X” incase using those make sure they are in lowercase. -* Issuer URL need to have “/” at the end https://gitlab.com/ -* “Cannot create a pool with the same name” - Requested entity already exists. How long can we not create it? -* "Permission \'iam.serviceAccounts.getAccessToken\' denied on resource (or it may not exist). +## Gitlab Prerequisites +* Create a Group in gitlab +* Add project called “folder factory” and copy code from [devops folder factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/folder-factory) into it +* Add project called “project factory” and copy code from [devops project factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/project-factory) into it +* Add a project called “skunkworks” and copy code from [devops skunkworks repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/skunkworks) into it. +* Available self hosted or SaaS Gitlab runners for each of the gitlab projects to run the pipelines. +Once Gitlab set is completed go to Folder Factory -is there any correlation between what we specify as and the principal set we provide permissions for From bccc591d4edbf43f632bd07a8d0b4cbd5d379b41 Mon Sep 17 00:00:00 2001 From: agutta Date: Wed, 1 Mar 2023 10:59:40 -0600 Subject: [PATCH 034/132] Update README.md --- examples/guardrails/gitlab/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index 8a65b9f..226825a 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -25,7 +25,7 @@ The actual implementation for granting impersonation access on the service accou * Add a project called “skunkworks” and copy code from [devops skunkworks repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/skunkworks) into it. * Available self hosted or SaaS Gitlab runners for each of the gitlab projects to run the pipelines. -Once Gitlab set is completed go to Folder Factory +Once Gitlab set is completed go to [Folder Factory](../folder-factory) From 88d79c0564f25041decc590484df89ec50bd9ec1 Mon Sep 17 00:00:00 2001 From: agutta Date: Wed, 1 Mar 2023 11:00:44 -0600 Subject: [PATCH 035/132] Update README.md --- examples/guardrails/gitlab/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index 226825a..8c4e676 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -25,7 +25,7 @@ The actual implementation for granting impersonation access on the service accou * Add a project called “skunkworks” and copy code from [devops skunkworks repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/skunkworks) into it. * Available self hosted or SaaS Gitlab runners for each of the gitlab projects to run the pipelines. -Once Gitlab set is completed go to [Folder Factory](../folder-factory) +Once Gitlab set is completed go to [Folder Factory](./folder-factory) From d3357cf624d8ae36aa4683ef3cfb0204eda2013a Mon Sep 17 00:00:00 2001 From: agutta Date: Wed, 1 Mar 2023 11:05:43 -0600 Subject: [PATCH 036/132] Update README.md --- .../gitlab/folder-factory/README.md | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/examples/guardrails/gitlab/folder-factory/README.md b/examples/guardrails/gitlab/folder-factory/README.md index aa5036b..f2be2b0 100644 --- a/examples/guardrails/gitlab/folder-factory/README.md +++ b/examples/guardrails/gitlab/folder-factory/README.md @@ -10,25 +10,6 @@ Using Keyless Authentication the project factory connects a defined Github repos The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. -## Repository Configuration -This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. - -If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw - -After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: - -Secret configuration - -or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: -``` -env: - STATE_BUCKET: 'XXXX' - # The GCS bucket to store the terraform state - WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' - # The workload identity provider that should be used for this repository. - SERVICE_ACCOUNT: 'XXXX@XXXX' - # The service account that should be used for this repository. -``` ## Setting up folders @@ -57,3 +38,41 @@ iam: ``` Every folder is defined with its own yaml file located in the following [Folder](data/folders). + +## How to run this stage +### Prerequisites + +Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. There is a sample code provided in “folder.yaml.sample” to create a folder and for terraform to create a folder minimum below permissions are required. +“Folder Creator” or “Folder Admin” at org level +“Organization Policy Admin” at org level + + +### Installation Steps +From the folder-factory Gitlab project page +* CICD configuration file path + Navigate to Settings > CICD > expand General pipelines + Update “CI/CD configuration file” value to the relative path of the gitlab-ci.yml file from the root directory + e.g. .gitlab/workflows/.gitlab-ci.yml + +* CI/CD variables + Navigate to Settings > CICD > expand Variables + Add the variables to the pipeline as described in the table below. + The same can be accessed from the README.md file under .gitlab/workflows in folder-factory. + +| Variable | Description | Sample value | +|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| GCP_PROJECT_ID | The GCP project ID of your service account | sample-project-1122 | +| GCP_SERVICE_ACCOUNT | The Service Account to be used for creating folders | xyz@sample-project-1122.iam.gserviceaccount.com | +| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects//locations/global/workloadIdentityPools//providers/ | +| STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | +| TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | +| TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | +| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | + + + + +Once the prerequisites are set up, any commit to the remote main branch with changes to *.tf, *.tfvars, data/*, modules/* files should trigger the pipeline. + + +.gcp-auth before-script should run successfully in the pipeline if the workload identity federation is configured as required. From d8665c039b063f01e87f0d7091b67275ce4377b3 Mon Sep 17 00:00:00 2001 From: agutta Date: Wed, 1 Mar 2023 11:08:10 -0600 Subject: [PATCH 037/132] Update README.md --- .../gitlab/folder-factory/README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/examples/guardrails/gitlab/folder-factory/README.md b/examples/guardrails/gitlab/folder-factory/README.md index f2be2b0..036a40a 100644 --- a/examples/guardrails/gitlab/folder-factory/README.md +++ b/examples/guardrails/gitlab/folder-factory/README.md @@ -63,16 +63,27 @@ From the folder-factory Gitlab project page |--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| | GCP_PROJECT_ID | The GCP project ID of your service account | sample-project-1122 | | GCP_SERVICE_ACCOUNT | The Service Account to be used for creating folders | xyz@sample-project-1122.iam.gserviceaccount.com | -| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects//locations/global/workloadIdentityPools//providers/ | -| STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | +| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_NAME}/providers/${PROVIDER_NAME} | +| STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket. Use a seed project if running this as part of Foundations or create a new GCS Bucket. | sample-terraform-state-bucket | | TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | | TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | | TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | +* Once the prerequisites are set up, any commit to the remote main branch with changes to *.tf, *.tfvars, data/*, modules/* files should trigger the pipeline. +* .gcp-auth before-script should run successfully in the pipeline if the workload identity federation is configured as required. -Once the prerequisites are set up, any commit to the remote main branch with changes to *.tf, *.tfvars, data/*, modules/* files should trigger the pipeline. +### Pipeline Workflow Overview +The complete workflow comprises of 4 stages and 2 before-script jobs + * before_script jobs : + * gcp-auth : creates the wif credentials by impersonating the service account. + * terraform init : initializes terraform in the specified TF_ROOT directory + * Stages: + * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages + * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command + * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts + * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once the plan is successful. + Runs terraform apply and creates the infrastructure specified. -.gcp-auth before-script should run successfully in the pipeline if the workload identity federation is configured as required. From 3b96e97c6c1790725062f2a57220094a94b1dc8c Mon Sep 17 00:00:00 2001 From: agutta Date: Wed, 1 Mar 2023 11:09:09 -0600 Subject: [PATCH 038/132] Update README.md --- examples/guardrails/gitlab/folder-factory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/folder-factory/README.md b/examples/guardrails/gitlab/folder-factory/README.md index 036a40a..0a742be 100644 --- a/examples/guardrails/gitlab/folder-factory/README.md +++ b/examples/guardrails/gitlab/folder-factory/README.md @@ -59,7 +59,7 @@ From the folder-factory Gitlab project page Add the variables to the pipeline as described in the table below. The same can be accessed from the README.md file under .gitlab/workflows in folder-factory. -| Variable | Description | Sample value | +| Variable | Description |Sample value | |--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| | GCP_PROJECT_ID | The GCP project ID of your service account | sample-project-1122 | | GCP_SERVICE_ACCOUNT | The Service Account to be used for creating folders | xyz@sample-project-1122.iam.gserviceaccount.com | From 7fe3397b82de34af9d9956809af8131298e37e6d Mon Sep 17 00:00:00 2001 From: agutta Date: Wed, 1 Mar 2023 11:28:04 -0600 Subject: [PATCH 039/132] Update README.md --- examples/guardrails/gitlab/folder-factory/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/guardrails/gitlab/folder-factory/README.md b/examples/guardrails/gitlab/folder-factory/README.md index 0a742be..9b724f7 100644 --- a/examples/guardrails/gitlab/folder-factory/README.md +++ b/examples/guardrails/gitlab/folder-factory/README.md @@ -38,6 +38,12 @@ iam: ``` Every folder is defined with its own yaml file located in the following [Folder](data/folders). +Copy "folder.yaml.sample" to "folder_name.yaml"; Name of the yaml file will be used to create folder with the same name. +Once folder_name.yaml file is created update yaml file + * parent - can be another folder or organization + * ServiceAccount +data/folders can have multiple yaml files and a folder will be created for each yaml file. + ## How to run this stage ### Prerequisites From bb1e79f6b66c03749b318ed01d0d61a0ca348bec Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Thu, 2 Mar 2023 14:51:45 +0530 Subject: [PATCH 040/132] azure devops file added --- .../folder-factory/azure-pipeline.yml | 142 +++++++----------- .../project-factory/azure-pipeline.yml | 125 ++++++--------- .../azuredevops/skunkworks/azure-pipeline.yml | 125 ++++++--------- 3 files changed, 155 insertions(+), 237 deletions(-) diff --git a/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml b/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml index 814a166..22538a0 100644 --- a/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml +++ b/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml @@ -1,124 +1,94 @@ -name: $(Date:yyyyMMdd)$(Rev:.r) +name: "$(Date:yyyyMMdd)$(Rev:.r)" trigger: - - none -pool: - vmImage: ubuntu-latest + branches: + include: + - main + + +variables: + azureSubscription: "" + projectID: "" + workloadIdentityPoolProvider: "" + Projectnumber: "" + serviceaccount: "" + workloadIdentityPools: "" + + +pool: + vmImage: "ubuntu-latest" + + stages: - stage: auth displayName: "GCP WIF Auth" jobs: - job: governance_pipeline - timeoutInMinutes: 20 + timeoutInMinutes: 30 steps: - task: charleszipp.azure-pipelines-tasks-terraform.azure-pipelines-tasks-terraform-installer.TerraformInstaller@0 - displayName: install terraform + displayName: "Install Terraform" inputs: - terraformVersion: latest + terraformVersion: "latest" - # AzureCLI task to retrieve an Azure token for the Service Principal and then exchanges it against a Service Account token using Workload Identity Federation - + # AzureCLI task to retrieve an Azure token for the Service Principal and then exchanges it against a Service Account token using Workload Identity Federation - task: AzureCLI@2 - displayName: get access token + displayName: "Get access token" inputs: - azureSubscription: 'azuretogcp' - scriptType: 'bash' - scriptLocation: 'inlineScript' + azureSubscription: "$(azureSubscription)" + scriptType: "bash" + scriptLocation: "inlineScript" inlineScript: | SUBJECT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt" SUBJECT_TOKEN=$(az account get-access-token --query accessToken --output tsv) STS_TOKEN=$(curl --silent -0 -X POST https://sts.googleapis.com/v1/token \ - -H 'Content-Type: text/json; charset=utf-8' \ + -H "Content-Type: text/json; charset=utf-8" \ -d @- <" + projectID: "" + workloadIdentityPoolProvider: "" + Projectnumber: "" + serviceaccount: "" + workloadIdentityPools: "" + + +pool: + vmImage: "ubuntu-latest" + + stages: - stage: auth displayName: "GCP WIF Auth" jobs: - job: governance_pipeline - timeoutInMinutes: 20 + timeoutInMinutes: 30 steps: - task: charleszipp.azure-pipelines-tasks-terraform.azure-pipelines-tasks-terraform-installer.TerraformInstaller@0 - displayName: install terraform + displayName: "Install Terraform" inputs: - terraformVersion: latest + terraformVersion: "latest" - # AzureCLI task to retrieve an Azure token for the Service Principal and then exchanges it against a Service Account token using Workload Identity Federation - + # AzureCLI task to retrieve an Azure token for the Service Principal and then exchanges it against a Service Account token using Workload Identity Federation - task: AzureCLI@2 - displayName: get access token + displayName: "Get access token" inputs: - azureSubscription: 'azuretogcp' - scriptType: 'bash' - scriptLocation: 'inlineScript' + azureSubscription: "$(azureSubscription)" + scriptType: "bash" + scriptLocation: "inlineScript" inlineScript: | SUBJECT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt" SUBJECT_TOKEN=$(az account get-access-token --query accessToken --output tsv) STS_TOKEN=$(curl --silent -0 -X POST https://sts.googleapis.com/v1/token \ - -H 'Content-Type: text/json; charset=utf-8' \ + -H "Content-Type: text/json; charset=utf-8" \ -d @- <" + projectID: "" + workloadIdentityPoolProvider: "" + Projectnumber: "" + serviceaccount: "" + workloadIdentityPools: "" + + +pool: + vmImage: "ubuntu-latest" + + stages: - stage: auth displayName: "GCP WIF Auth" jobs: - job: governance_pipeline - timeoutInMinutes: 20 + timeoutInMinutes: 30 steps: - task: charleszipp.azure-pipelines-tasks-terraform.azure-pipelines-tasks-terraform-installer.TerraformInstaller@0 - displayName: install terraform + displayName: "Install Terraform" inputs: - terraformVersion: latest + terraformVersion: "latest" - # AzureCLI task to retrieve an Azure token for the Service Principal and then exchanges it against a Service Account token using Workload Identity Federation - + # AzureCLI task to retrieve an Azure token for the Service Principal and then exchanges it against a Service Account token using Workload Identity Federation - task: AzureCLI@2 - displayName: get access token + displayName: "Get access token" inputs: - azureSubscription: 'azuretogcp' - scriptType: 'bash' - scriptLocation: 'inlineScript' + azureSubscription: "$(azureSubscription)" + scriptType: "bash" + scriptLocation: "inlineScript" inlineScript: | SUBJECT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt" SUBJECT_TOKEN=$(az account get-access-token --query accessToken --output tsv) STS_TOKEN=$(curl --silent -0 -X POST https://sts.googleapis.com/v1/token \ - -H 'Content-Type: text/json; charset=utf-8' \ + -H "Content-Type: text/json; charset=utf-8" \ -d @- < Date: Fri, 3 Mar 2023 06:53:25 -0600 Subject: [PATCH 041/132] Update README.md --- .../gitlab/project-factory/README.md | 69 +++++++++++++------ 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/examples/guardrails/gitlab/project-factory/README.md b/examples/guardrails/gitlab/project-factory/README.md index f901e7f..601a0a0 100644 --- a/examples/guardrails/gitlab/project-factory/README.md +++ b/examples/guardrails/gitlab/project-factory/README.md @@ -12,27 +12,6 @@ Using Keyless Authentication the project factory connects a defined Github repos The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. -## Repository Configuration -This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. - -If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw - -After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: - -Secret settings - -or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: -``` -env: - STATE_BUCKET: 'XXXX' - # The GCS bucket to store the terraform state - FOLDER: 'folders/XXXX' - # The folder under which the projects should be created - WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' - # The workload identity provider that should be used for this repository. - SERVICE_ACCOUNT: 'XXXX@XXXX' - # The service account that should be used for this repository. -``` ## Setting up projects @@ -73,3 +52,51 @@ repo_branch: dev ``` Every project is defined with its own file located in the [Project Folder](data/projects). + +## How to run this stage + +### Prerequisites +The parent folders are provisioned to place the projects. + + +Workload Identity setup between the project factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create projects, workload identity pools and providers, service accounts and IAM bindings on the service accounts under the parent folder in which the projects are to be created. +“Project Creator” should already be granted when running the folder factory. +“Billing User” on Billing Account + +### Installation Steps +From the project-factory Gitlab project page +CICD configuration file path +Navigate to Settings > CICD > expand General pipelines +Update “CI/CD configuration file” value to the relative path of the gitlab-ci.yml file from the root directory +e.g. .gitlab/workflows/.gitlab-ci.yml + +CI/CD variables +Navigate to Settings > CICD > expand Variables +Add the variables to the pipeline as described in the table below. The same can be accessed from the README.md file under .gitlab/workflows in project-factory. + +| Variable | Description | Sample value | +|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| GCP_PROJECT_ID | The GCP project ID of your service account | sample-project-1122 | +| GCP_SERVICE_ACCOUNT | The Service Account to be used for creating projects | xyz@sample-project-1122.iam.gserviceaccount.com | +| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_NAME}/providers/${PROVIDER_NAME} | +| STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | +| TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | +| TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | +| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | +Similar to Folder factory, + +Once the prerequisites are set up, any commit to the remote main branch with changes to *.tf, *.tfvars, data/*, modules/* files should trigger the pipeline. + +.gcp-auth before-script should run successfully in the pipeline if the workload identity federation is configured as required. + +### Pipeline Workflow Overview +The complete workflow comprises of 4 stages and 2 before-script jobs +* before_script jobs : + * gcp-auth : creates the wif credentials by impersonating the service account. + * terraform init : initializes terraform in the specified TF_ROOT directory +* Stages: + * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages + * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command + * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts + * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once the plan is successful. + Runs terraform apply and creates the infrastructure specified. From a778505430cf4db006e8667910321e2f94682506 Mon Sep 17 00:00:00 2001 From: agutta Date: Fri, 3 Mar 2023 06:54:34 -0600 Subject: [PATCH 042/132] Update README.md --- examples/guardrails/gitlab/project-factory/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/gitlab/project-factory/README.md b/examples/guardrails/gitlab/project-factory/README.md index 601a0a0..63f0cf4 100644 --- a/examples/guardrails/gitlab/project-factory/README.md +++ b/examples/guardrails/gitlab/project-factory/README.md @@ -82,7 +82,8 @@ Add the variables to the pipeline as described in the table below. The same can | STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | | TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | | TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | -| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | +| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | | + Similar to Folder factory, Once the prerequisites are set up, any commit to the remote main branch with changes to *.tf, *.tfvars, data/*, modules/* files should trigger the pipeline. @@ -93,7 +94,7 @@ Once the prerequisites are set up, any commit to the remote main branch with cha The complete workflow comprises of 4 stages and 2 before-script jobs * before_script jobs : * gcp-auth : creates the wif credentials by impersonating the service account. - * terraform init : initializes terraform in the specified TF_ROOT directory + * terraform init : initializes terraform in the specified TF_ROOT directory * Stages: * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command From 6af5fe8f2c933d8eb8b15e4abd4d7ee137ac103f Mon Sep 17 00:00:00 2001 From: agutta Date: Fri, 3 Mar 2023 06:55:00 -0600 Subject: [PATCH 043/132] Update README.md --- examples/guardrails/gitlab/project-factory/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/guardrails/gitlab/project-factory/README.md b/examples/guardrails/gitlab/project-factory/README.md index 63f0cf4..895021d 100644 --- a/examples/guardrails/gitlab/project-factory/README.md +++ b/examples/guardrails/gitlab/project-factory/README.md @@ -96,8 +96,8 @@ The complete workflow comprises of 4 stages and 2 before-script jobs * gcp-auth : creates the wif credentials by impersonating the service account. * terraform init : initializes terraform in the specified TF_ROOT directory * Stages: - * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages - * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command - * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts - * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once the plan is successful. + * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages + * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command + * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts + * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once the plan is successful. Runs terraform apply and creates the infrastructure specified. From 75c8740815af050afc6b381a79439d7b30bdc916 Mon Sep 17 00:00:00 2001 From: agutta Date: Fri, 3 Mar 2023 06:56:00 -0600 Subject: [PATCH 044/132] Update README.md --- .../.gitlab/workflows/README.md | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/examples/guardrails/gitlab/project-factory/.gitlab/workflows/README.md b/examples/guardrails/gitlab/project-factory/.gitlab/workflows/README.md index d682232..8b13789 100644 --- a/examples/guardrails/gitlab/project-factory/.gitlab/workflows/README.md +++ b/examples/guardrails/gitlab/project-factory/.gitlab/workflows/README.md @@ -1,45 +1 @@ -# Terraform pipeline execution with Gitlab runners and Gitlab repository - -This workflow runs terraform pipelines using Gitlab CICD. This document covers the steps to setup Gitlab CICD and provides a high level overview of the stages involved: - -## Prerequisites - -* A Gitlab project containing the project-factory repository -* Available Gitlab Runners for the project either self hosted or the SaaS Gitlab shared runners. -* A working WIF provider, pool setup with audience as gitlab.com with neccessary attributes and linked service account with required permissions. For further details, please refer to the official [Gitlab Documentation](https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/) - - -## Setup - -1. Update the CICD configuration file path in the repository - * From the project-factory Gitlab project page, Navigate to Settings > CICD > expand General pipelines - * update CI/CD configuration file value to the relative path of the gitlab-ci.yml file from the root directory - -2. Update the CI/CD variables - * From the project-factory project page, Navigate to Settings > CICD > expand Variables - * Add the below variables to the pipeline - -| Variable | Description | Sample value | -|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| -| GCP_PROJECT_ID | The GCP project ID of your service account | sample-project-1122 | -| GCP_SERVICE_ACCOUNT | The Service Account to be used for creating projects | xyz@sample-project-1122.iam.gserviceaccount.com | -| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects//locations/global/workloadIdentityPools//providers/ | -| STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | -| TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | -| TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | -| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | - -## Overview of the Pipeline stages -The complete workflow consists of 4 stages and 2 before-script jobs - -* before_script jobs : - * gcp-auth : creates the wif credentials by impersonating the service account. - * terraform init : initializes terraform in the specified TF_ROOT directory - -* Stages: - * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages - * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command - * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts - * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once plan is successful. Runs terraform apply and creates the infrastructure specified. - From b5b87192d36121301ae4f91742e54140dc15a7e6 Mon Sep 17 00:00:00 2001 From: agutta Date: Fri, 3 Mar 2023 06:58:39 -0600 Subject: [PATCH 045/132] Update README.md --- .../guardrails/gitlab/skunkworks/README.md | 62 +++++++++++++------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/examples/guardrails/gitlab/skunkworks/README.md b/examples/guardrails/gitlab/skunkworks/README.md index 0b89bb2..7f75f2c 100644 --- a/examples/guardrails/gitlab/skunkworks/README.md +++ b/examples/guardrails/gitlab/skunkworks/README.md @@ -8,22 +8,48 @@ The idea is to enable developers of the "skunkworks" repository to deploy into t This template creates a bucket in the specified target environment. -## Repository Configuration -This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. +## How to run this stage + +### Prerequisites +Project factory is executed successfully and the respective service accounts for all the environments and projects are in place. + + +The branch structure should mirror the environments that are going to be deployed. For example, for deploying resources in dev, staging and prod skunkworks projects, three protected branches for dev, staging and prod are required. + + +### Installation Steps +Update the CICD configuration file path in the repository + * From the skunkworks Gitlab project page, Navigate to Settings > CICD > expand General pipelines + * update CI/CD configuration file value to the relative path of the gitlab-ci.yml file from the root directory + +2. Update the CI/CD variables + * From the skunkworks project page, Navigate to Settings > CICD > expand Variables + * Add the below variables to the pipeline + +| Variable | Description | Sample value | +|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| DEV_GCP_PROJECT_ID | The GCP project ID in which resources are to be created on a push event to dev branch | sample-dev-project-1122 | +| DEV_GCP_SERVICE_ACCOUNT | The Service Account of the dev gcp project configured with Workload Identity Federation (WIF) | xyz@sample-dev-project-1122.iam.gserviceaccount.com | +| STAGE_GCP_PROJECT_ID | The GCP project ID in which resources are to be created on a push event to staging branch | sample-stage-project-1122 | +| STAGE_GCP_SERVICE_ACCOUNT | The Service Account of the staging gcp project configured with Workload Identity Federation (WIF) | xyz@sample-stage-project-1122.iam.gserviceaccount.com | +| PROD_GCP_PROJECT_ID | The GCP project ID in which resources are to be created on a push event to prod branch | sample-prod-project-1122 | +| PROD_GCP_SERVICE_ACCOUNT | The Service Account of the prod gcp project configured with Workload Identity Federation (WIF) | xyz@sample-prod-project-1122.iam.gserviceaccount.com | +| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects//locations/global/workloadIdentityPools//providers/ | +| STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | +| TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | +| TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | +| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | | + +## Pipeline Workflow Overview +The complete workflow contains a parent child pipeline. The parent(.gitlab-ci.yaml) file is the trigger stage for each of the environments. It passes relevant variables for that environment to the child pipeline which executes the core terraform workflow. The child pipeline workflow executes 4 stages and 2 before-script jobs + +* before_script jobs : + * gcp-auth : creates the wif credentials by impersonating the service account. + * terraform init : initializes terraform in the specified TF_ROOT directory +* Stages: + * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages + * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command + * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts + * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once the plan is successful. + Runs terraform apply and creates the infrastructure specified. -If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw - -After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: - -Secret configuration - -or by modifing the [Workflow Action Files](.github/workflows/) and setting the environment variables: -``` -env: - STATE_BUCKET: 'XXXX' - # The GCS bucket to store the terraform state - WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' - # The workload identity provider that should be used for this repository. - SERVICE_ACCOUNT: 'XXXX@XXXX' - # The service account that should be used for this repository. -``` From 95ef5d85e092ec29e2763c0bba118696c0c1fb63 Mon Sep 17 00:00:00 2001 From: agutta Date: Fri, 3 Mar 2023 06:59:24 -0600 Subject: [PATCH 046/132] Update README.md --- examples/guardrails/gitlab/skunkworks/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/skunkworks/README.md b/examples/guardrails/gitlab/skunkworks/README.md index 7f75f2c..6f21ac6 100644 --- a/examples/guardrails/gitlab/skunkworks/README.md +++ b/examples/guardrails/gitlab/skunkworks/README.md @@ -34,7 +34,7 @@ Update the CICD configuration file path in the repository | STAGE_GCP_SERVICE_ACCOUNT | The Service Account of the staging gcp project configured with Workload Identity Federation (WIF) | xyz@sample-stage-project-1122.iam.gserviceaccount.com | | PROD_GCP_PROJECT_ID | The GCP project ID in which resources are to be created on a push event to prod branch | sample-prod-project-1122 | | PROD_GCP_SERVICE_ACCOUNT | The Service Account of the prod gcp project configured with Workload Identity Federation (WIF) | xyz@sample-prod-project-1122.iam.gserviceaccount.com | -| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects//locations/global/workloadIdentityPools//providers/ | +| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_NAME}/providers/${PROVIDER_NAME} | | STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | | TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | | TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | From 883beab03c00041c56df47cc53dfe079494794c2 Mon Sep 17 00:00:00 2001 From: agutta Date: Fri, 3 Mar 2023 07:00:04 -0600 Subject: [PATCH 047/132] Update README.md --- .../skunkworks/.gitlab/workflows/README.md | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/README.md b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/README.md index b17b997..8b13789 100644 --- a/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/README.md +++ b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/README.md @@ -1,53 +1 @@ -# Terraform pipeline execution with Gitlab runners and Gitlab repository - -This workflow runs terraform pipelines using Gitlab CICD. This document covers the steps to setup Gitlab CICD and provides a high level overview of the stages involved: - -## Prerequisites - -* A Gitlab project containing the skunkworks repository -* Available Gitlab Runners for the project either self hosted or the SaaS Gitlab shared runners. -* Project-factory is executed successfully - - -## Overview -This workflow can execute terraform on different GCP projects depending on the branch on which push event is executed. Supports dev, staging and prod branches. The .gitlab-ci.yml file triggers the child pipeline on the respective branches and runs the workflow as specified in the workflow.yml file to execute terraform. All the three branches dev, staging and prod are linked to specific projects with DEV_, STAGE_, PROD_ CI/CD variables as specified in Gitlab project. For more information, see the setup section below - - -## Setup - -1. Update the CICD configuration file path in the repository - * From the skunkworks Gitlab project page, Navigate to Settings > CICD > expand General pipelines - * update CI/CD configuration file value to the relative path of the gitlab-ci.yml file from the root directory - -2. Update the CI/CD variables - * From the skunkworks project page, Navigate to Settings > CICD > expand Variables - * Add the below variables to the pipeline - -| Variable | Description | Sample value | -|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| -| DEV_GCP_PROJECT_ID | The GCP project ID in which resources are to be created on a push event to dev branch | sample-dev-project-1122 | -| DEV_GCP_SERVICE_ACCOUNT | The Service Account of the dev gcp project configured with Workload Identity Federation (WIF) | xyz@sample-dev-project-1122.iam.gserviceaccount.com | -| STAGE_GCP_PROJECT_ID | The GCP project ID in which resources are to be created on a push event to staging branch | sample-stage-project-1122 | -| STAGE_GCP_SERVICE_ACCOUNT | The Service Account of the staging gcp project configured with Workload Identity Federation (WIF) | xyz@sample-stage-project-1122.iam.gserviceaccount.com | -| PROD_GCP_PROJECT_ID | The GCP project ID in which resources are to be created on a push event to prod branch | sample-prod-project-1122 | -| PROD_GCP_SERVICE_ACCOUNT | The Service Account of the prod gcp project configured with Workload Identity Federation (WIF) | xyz@sample-prod-project-1122.iam.gserviceaccount.com | -| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects//locations/global/workloadIdentityPools//providers/ | -| STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | -| TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | -| TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | -| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | | - -## Overview of the Pipeline stages -The complete workflow contains a parent child pipeline with the parent containing just the trigger stage for each of the environments that passes relevant variables for that environment to the child pipeline which executes the core terraform workflow. The child pipeline [workflow](./workflow.yml) has 4 stages and 2 before-script jobs - -* before_script jobs : - * gcp-auth : creates the wif credentials by impersonating the service account. - * terraform init : initializes terraform in the specified TF_ROOT directory - -* Stages: - * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages - * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command - * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts - * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once plan is successful. Runs terraform apply and creates the infrastructure specified. - From 7afcbc631a320a75d121d92600468cad27acebb4 Mon Sep 17 00:00:00 2001 From: agutta Date: Fri, 3 Mar 2023 07:03:07 -0600 Subject: [PATCH 048/132] Update README.md --- examples/guardrails/gitlab/skunkworks/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/skunkworks/README.md b/examples/guardrails/gitlab/skunkworks/README.md index 6f21ac6..2375940 100644 --- a/examples/guardrails/gitlab/skunkworks/README.md +++ b/examples/guardrails/gitlab/skunkworks/README.md @@ -18,7 +18,7 @@ The branch structure should mirror the environments that are going to be deploye ### Installation Steps -Update the CICD configuration file path in the repository +1. Update the CICD configuration file path in the repository * From the skunkworks Gitlab project page, Navigate to Settings > CICD > expand General pipelines * update CI/CD configuration file value to the relative path of the gitlab-ci.yml file from the root directory From af1651f6590ba5ea03725be0e9911bfbc4f21682 Mon Sep 17 00:00:00 2001 From: agutta Date: Fri, 3 Mar 2023 08:23:32 -0600 Subject: [PATCH 049/132] Update README.md --- examples/guardrails/gitlab/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index 8c4e676..ddd760d 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -4,10 +4,6 @@ The setup involves setting up gitlab repository and the corresponding CI/CD sett The pipeline triggers based on select events (like push to specific branches), authenticates to the specified service account using Workload Identity federation and runs the pipeline to deploy infrastructure using terraform in GCP. Please look through the [README](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/README.md) for an overview of the CICD process. -## High level Architecture -

- Stages diagram -

## Implementation Process From 0c4b130adb905e864fafdd564ba0e3ba37f209cc Mon Sep 17 00:00:00 2001 From: agutta Date: Sun, 5 Mar 2023 14:09:27 -0600 Subject: [PATCH 050/132] Update README.md --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bc7663f..bddeb98 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,21 @@ DevOps governance will give infrastructure teams the required flexibility whilst To demonstrate how to enforce guardrails in Google Cloud we provide the Guardrail Examples: -![Guardrail Examples](https://user-images.githubusercontent.com/94000358/169811919-e5c36181-c1d2-4339-8103-d86640e9a1f1.png) - -- The [Folder Factory](/examples/guardrails/folder-factory) sets guardrails in the form of organisational policies on folders. +- The [Folder Factory](/examples/guardrails/folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. - The [Project Factory](/examples/guardrails/project-factory) sets up projects for teams. For this it creates a deployment service account, links this to a Github repository and defines the roles and permissions that the deployment service account has. - The [Skunkworks - IaC Kickstarter](/examples/guardrails/skunkworks) is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. +![Guardrail Examples](https://user-images.githubusercontent.com/94000358/169811919-e5c36181-c1d2-4339-8103-d86640e9a1f1.png) + +## Workload Identity federation +Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. +This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. + +examples/guardrails section covers different CICD tools and how to leverage Workload Identity Federation between each tool and Google Cloud. + + ## Disclaimer This is not an officially supported Google product. From 0ad9aad4000d73b2648a6fa1069c371a00180df5 Mon Sep 17 00:00:00 2001 From: agutta Date: Sun, 5 Mar 2023 15:00:53 -0600 Subject: [PATCH 051/132] Update README.md --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bddeb98..07a4ae1 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,20 @@ To demonstrate how to enforce guardrails in Google Cloud we provide the Guardrai Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. -examples/guardrails section covers different CICD tools and how to leverage Workload Identity Federation between each tool and Google Cloud. +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +### High Level Process +* GCP + - Create a Workload Identity Pool + - Create a Workload Identity Provider + - Create a Service Account and grant permissions + +* CICD tool + - Specify where the pipeline configuration file resides + - Configure variables to pass relevant information to GCP to genrate short-lived tokens + +examples/guardrails section covers different CICD tools and how to leverage Workload Identity Federation between each tool and Google Cloud. ## Disclaimer From 4a0b0a602d80cd61719310435cb00f0641be42c0 Mon Sep 17 00:00:00 2001 From: Kodanda Rama Date: Wed, 8 Mar 2023 15:06:20 +0530 Subject: [PATCH 052/132] add policy-validate state --- .../.gitlab/workflows/.gitlab-ci.yml | 35 +++++++++++++++++++ .../gitlab/folder-factory/README.md | 7 ++-- .../.gitlab/workflows/.gitlab-ci.yml | 35 +++++++++++++++++++ .../gitlab/project-factory/README.md | 7 ++-- .../.gitlab/workflows/.gitlab-ci.yml | 3 ++ .../skunkworks/.gitlab/workflows/workflow.yml | 33 +++++++++++++++++ .../guardrails/gitlab/skunkworks/README.md | 7 ++-- 7 files changed, 121 insertions(+), 6 deletions(-) diff --git a/examples/guardrails/gitlab/folder-factory/.gitlab/workflows/.gitlab-ci.yml b/examples/guardrails/gitlab/folder-factory/.gitlab/workflows/.gitlab-ci.yml index dceaa74..cf6bb06 100644 --- a/examples/guardrails/gitlab/folder-factory/.gitlab/workflows/.gitlab-ci.yml +++ b/examples/guardrails/gitlab/folder-factory/.gitlab/workflows/.gitlab-ci.yml @@ -26,12 +26,15 @@ variables: GCP_PROJECT_ID: $GCP_PROJECT_ID GCP_WORKLOAD_IDENTITY_PROVIDER: $GCP_WORKLOAD_IDENTITY_PROVIDER GCP_SERVICE_ACCOUNT: $GCP_SERVICE_ACCOUNT + TERRAFORM_POLICY_VALIDATE: $TERRAFORM_POLICY_VALIDATE + POLICY_LIBRARY_REPO : $POLICY_LIBRARY_REPO # Provides a list of stages for this GitLab workflow stages: - setup-terraform - validate - plan + - policy-validate - apply .gcp-auth: &gcp-auth @@ -117,6 +120,38 @@ plan: - ${TF_ROOT}/$TF_PLAN_JSON expire_in: 7 days #optional. Gitlab stores artifacts of successful pipelines for the most recent commit on each ref. If needed, enable "Keep artifacts from most recent successful jobs" in CI/CD settings of the repository. +policy-validate: + stage: policy-validate + dependencies: + - setup-terraform + - plan + before_script: + - *gcp-auth + - *terraform-ver-init + - apt-get install google-cloud-sdk-terraform-tools -y + - git clone $POLICY_LIBRARY_REPO $TF_ROOT/policy-repo + script: + - | + cd $TF_ROOT + terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json + ls -l $TF_ROOT/policy-repo + violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) + ret_val=$? + if [ $ret_val -eq 2 ]; + then + echo "$violations" + echo "Violations found, not proceeding with terraform apply" + exit 1 + elif [ $ret_val -ne 0 ]; + then + echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" + exit 1 + else + echo "No policy violations detected; proceeding with terraform apply" + fi + rules: + - if: '$TERRAFORM_POLICY_VALIDATE == "true"' + #Stage:apply | job: apply # purpose: executes the plan from the file created in the plan stage apply: diff --git a/examples/guardrails/gitlab/folder-factory/README.md b/examples/guardrails/gitlab/folder-factory/README.md index 9b724f7..926eeba 100644 --- a/examples/guardrails/gitlab/folder-factory/README.md +++ b/examples/guardrails/gitlab/folder-factory/README.md @@ -73,7 +73,9 @@ From the folder-factory Gitlab project page | STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket. Use a seed project if running this as part of Foundations or create a new GCS Bucket. | sample-terraform-state-bucket | | TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | | TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | -| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | +| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 +| TERRAFORM_POLICY_VALIDATE | Set this value as true if terraform vet is to be run against the policy library repository set in $POLICY_LIBRARY_REPO variable | true | +| POLICY_LIBRARY_REPO | The policy library repository URL which will be cloned using git clone to run gcloud terraform vet against. | https://github.com/GoogleCloudPlatform/policy-library | * Once the prerequisites are set up, any commit to the remote main branch with changes to *.tf, *.tfvars, data/*, modules/* files should trigger the pipeline. @@ -81,7 +83,7 @@ From the folder-factory Gitlab project page * .gcp-auth before-script should run successfully in the pipeline if the workload identity federation is configured as required. ### Pipeline Workflow Overview -The complete workflow comprises of 4 stages and 2 before-script jobs +The complete workflow comprises of 4-5 stages and 2 before-script jobs * before_script jobs : * gcp-auth : creates the wif credentials by impersonating the service account. * terraform init : initializes terraform in the specified TF_ROOT directory @@ -89,6 +91,7 @@ The complete workflow comprises of 4 stages and 2 before-script jobs * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts + * policy-validate: Runs gcloud terraform vet against the terraform code with the constraints in the specified repository. * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once the plan is successful. Runs terraform apply and creates the infrastructure specified. diff --git a/examples/guardrails/gitlab/project-factory/.gitlab/workflows/.gitlab-ci.yml b/examples/guardrails/gitlab/project-factory/.gitlab/workflows/.gitlab-ci.yml index dceaa74..cf6bb06 100644 --- a/examples/guardrails/gitlab/project-factory/.gitlab/workflows/.gitlab-ci.yml +++ b/examples/guardrails/gitlab/project-factory/.gitlab/workflows/.gitlab-ci.yml @@ -26,12 +26,15 @@ variables: GCP_PROJECT_ID: $GCP_PROJECT_ID GCP_WORKLOAD_IDENTITY_PROVIDER: $GCP_WORKLOAD_IDENTITY_PROVIDER GCP_SERVICE_ACCOUNT: $GCP_SERVICE_ACCOUNT + TERRAFORM_POLICY_VALIDATE: $TERRAFORM_POLICY_VALIDATE + POLICY_LIBRARY_REPO : $POLICY_LIBRARY_REPO # Provides a list of stages for this GitLab workflow stages: - setup-terraform - validate - plan + - policy-validate - apply .gcp-auth: &gcp-auth @@ -117,6 +120,38 @@ plan: - ${TF_ROOT}/$TF_PLAN_JSON expire_in: 7 days #optional. Gitlab stores artifacts of successful pipelines for the most recent commit on each ref. If needed, enable "Keep artifacts from most recent successful jobs" in CI/CD settings of the repository. +policy-validate: + stage: policy-validate + dependencies: + - setup-terraform + - plan + before_script: + - *gcp-auth + - *terraform-ver-init + - apt-get install google-cloud-sdk-terraform-tools -y + - git clone $POLICY_LIBRARY_REPO $TF_ROOT/policy-repo + script: + - | + cd $TF_ROOT + terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json + ls -l $TF_ROOT/policy-repo + violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) + ret_val=$? + if [ $ret_val -eq 2 ]; + then + echo "$violations" + echo "Violations found, not proceeding with terraform apply" + exit 1 + elif [ $ret_val -ne 0 ]; + then + echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" + exit 1 + else + echo "No policy violations detected; proceeding with terraform apply" + fi + rules: + - if: '$TERRAFORM_POLICY_VALIDATE == "true"' + #Stage:apply | job: apply # purpose: executes the plan from the file created in the plan stage apply: diff --git a/examples/guardrails/gitlab/project-factory/README.md b/examples/guardrails/gitlab/project-factory/README.md index 895021d..a53e9d3 100644 --- a/examples/guardrails/gitlab/project-factory/README.md +++ b/examples/guardrails/gitlab/project-factory/README.md @@ -82,7 +82,9 @@ Add the variables to the pipeline as described in the table below. The same can | STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | | TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | | TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | -| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | | +| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | +| TERRAFORM_POLICY_VALIDATE | Set this value as true if terraform vet is to be run against the policy library repository set in $POLICY_LIBRARY_REPO variable | true | +| POLICY_LIBRARY_REPO | The policy library repository URL which will be cloned using git clone to run gcloud terraform vet against. | https://github.com/GoogleCloudPlatform/policy-library | Similar to Folder factory, @@ -91,7 +93,7 @@ Once the prerequisites are set up, any commit to the remote main branch with cha .gcp-auth before-script should run successfully in the pipeline if the workload identity federation is configured as required. ### Pipeline Workflow Overview -The complete workflow comprises of 4 stages and 2 before-script jobs +The complete workflow comprises of 4-5 stages and 2 before-script jobs * before_script jobs : * gcp-auth : creates the wif credentials by impersonating the service account. * terraform init : initializes terraform in the specified TF_ROOT directory @@ -99,5 +101,6 @@ The complete workflow comprises of 4 stages and 2 before-script jobs * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts + * policy-validate: Runs gcloud terraform vet against the terraform code with the constraints in the specified repository. * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once the plan is successful. Runs terraform apply and creates the infrastructure specified. diff --git a/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/.gitlab-ci.yml b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/.gitlab-ci.yml index ed1219c..17e0389 100644 --- a/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/.gitlab-ci.yml +++ b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/.gitlab-ci.yml @@ -7,6 +7,9 @@ variables: TF_VERSION: $TF_VERSION TF_LOG: $TF_LOG TF_ROOT: $TF_ROOT + TERRAFORM_POLICY_VALIDATE: $TERRAFORM_POLICY_VALIDATE + POLICY_LIBRARY_REPO : $POLICY_LIBRARY_REPO + trigger_dev: variables: diff --git a/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/workflow.yml b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/workflow.yml index 9059561..c5936ad 100644 --- a/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/workflow.yml +++ b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/workflow.yml @@ -15,6 +15,7 @@ stages: - setup-terraform - validate - plan + - policy-validate - apply .gcp-auth: &gcp-auth @@ -98,6 +99,38 @@ plan: - ${TF_ROOT}/$TF_PLAN_JSON expire_in: 7 days #optional. Gitlab stores artifacts of successful pipelines for the most recent commit on each ref. If needed, enable "Keep artifacts from most recent successful jobs" in CI/CD settings of the repository. +policy-validate: + stage: policy-validate + dependencies: + - setup-terraform + - plan + before_script: + - *gcp-auth + - *terraform-ver-init + - apt-get install google-cloud-sdk-terraform-tools -y + - git clone $POLICY_LIBRARY_REPO $TF_ROOT/policy-repo + script: + - | + cd $TF_ROOT + terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json + ls -l $TF_ROOT/policy-repo + violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) + ret_val=$? + if [ $ret_val -eq 2 ]; + then + echo "$violations" + echo "Violations found, not proceeding with terraform apply" + exit 1 + elif [ $ret_val -ne 0 ]; + then + echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" + exit 1 + else + echo "No policy violations detected; proceeding with terraform apply" + fi + rules: + - if: '$TERRAFORM_POLICY_VALIDATE == "true"' + #Stage:apply | job: apply # purpose: executes the plan from the file created in the plan stage apply: diff --git a/examples/guardrails/gitlab/skunkworks/README.md b/examples/guardrails/gitlab/skunkworks/README.md index 2375940..10a4bce 100644 --- a/examples/guardrails/gitlab/skunkworks/README.md +++ b/examples/guardrails/gitlab/skunkworks/README.md @@ -38,10 +38,12 @@ The branch structure should mirror the environments that are going to be deploye | STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | | TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | | TF_ROOT | The directory of the terraform code to be executed. Can be a path string or also a pre-defined gitlab CI variables | $CI_PROJECT_DIR | -| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | | +| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 +| TERRAFORM_POLICY_VALIDATE | Set this value as true if terraform vet is to be run against the policy library repository set in $POLICY_LIBRARY_REPO variable | true | +| POLICY_LIBRARY_REPO | The policy library repository URL which will be cloned using git clone to run gcloud terraform vet against. | https://github.com/GoogleCloudPlatform/policy-library | | ## Pipeline Workflow Overview -The complete workflow contains a parent child pipeline. The parent(.gitlab-ci.yaml) file is the trigger stage for each of the environments. It passes relevant variables for that environment to the child pipeline which executes the core terraform workflow. The child pipeline workflow executes 4 stages and 2 before-script jobs +The complete workflow contains a parent child pipeline. The parent(.gitlab-ci.yaml) file is the trigger stage for each of the environments. It passes relevant variables for that environment to the child pipeline which executes the core terraform workflow. The child pipeline workflow executes 4-5 stages and 2 before-script jobs * before_script jobs : * gcp-auth : creates the wif credentials by impersonating the service account. @@ -50,6 +52,7 @@ The complete workflow contains a parent child pipeline. The parent(.gitlab-ci.ya * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts + * policy-validate: Runs gcloud terraform vet against the terraform code with the constraints in the specified repository. * apply: This step is currently set as manual to be triggered from the Gitlab pipelines UI once the plan is successful. Runs terraform apply and creates the infrastructure specified. From d2d81b33efba08fe75bc2b1265a9c36b1d1d6f72 Mon Sep 17 00:00:00 2001 From: Kodanda Rama Date: Wed, 8 Mar 2023 15:06:56 +0530 Subject: [PATCH 053/132] add policy-validate stage From e8c89495c00aca11433c1d3ba4e58233eb682f7c Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 01:40:02 +0100 Subject: [PATCH 054/132] Added new supported platforms --- README.md | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 07a4ae1..9c73ab8 100644 --- a/README.md +++ b/README.md @@ -14,23 +14,37 @@ By making use of Gitlab or Github (or any other tools that offer protected branc DevOps governance will give infrastructure teams the required flexibility whilst still adhering to security requirements with “guardrails”. -## Guardrail Examples +## Guardrail & pipeline examples for individual workloads -To demonstrate how to enforce guardrails in Google Cloud we provide the Guardrail Examples: +To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. We provide you with the following 3 different components: -- The [Folder Factory](/examples/guardrails/folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. +![Guardrail Examples](https://user-images.githubusercontent.com/94000358/169811919-e5c36181-c1d2-4339-8103-d86640e9a1f1.png) -- The [Project Factory](/examples/guardrails/project-factory) sets up projects for teams. For this it creates a deployment service account, links this to a Github repository and defines the roles and permissions that the deployment service account has. +- The [Folder Factory](/examples/guardrails/github/folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. -- The [Skunkworks - IaC Kickstarter](/examples/guardrails/skunkworks) is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. +- The [Project Factory](/examples/guardrails/github/project-factory) sets up projects for teams. For this it creates a deployment service account, links this to a Github repository and defines the roles and permissions that the deployment service account has. -![Guardrail Examples](https://user-images.githubusercontent.com/94000358/169811919-e5c36181-c1d2-4339-8103-d86640e9a1f1.png) +The Folder Factory and the Project Factory are usually maintained centrally (by a cloud platform team) and used to manage the individual workloads. + +- The [Skunkworks - IaC Kickstarter](/examples/guardrails/github/skunkworks) is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. + +A video tutorial covering how to set up the guardrails for Github can be found here: https://www.youtube.com/watch?v=bbUNsjk6G7I + +The instructions above set out how to implement the Guardrail Examples for Github. We do however also provide support for other platforms: + +## Supported Platforms + + - [Bitbucket](/examples/guardrails/bitbucket/README.md) + - [Cloudbuild](/examples/guardrails/cloudbuild/README.md) + - [Github](/examples/guardrails/github/README.md) + - [Gitlab](/examples/guardrails/gitlab/README.md) + - [Jenkins](/examples/guardrails/jenkins/README.md) + - [Terraform-Cloud](/examples/guardrails/terraform-cloud/README.md) ## Workload Identity federation Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. - If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw ### High Level Process From df2236b02195ae83aef3887381736949f7473937 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:00:25 +0100 Subject: [PATCH 055/132] Update README.md --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c73ab8..8c8cff8 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ The Folder Factory and the Project Factory are usually maintained centrally (by - The [Skunkworks - IaC Kickstarter](/examples/guardrails/github/skunkworks) is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. +This template is based on an "ideal" initial pipeline which is as follows: + +![Ideal Pipeline Generic](https://user-images.githubusercontent.com/94000358/224195779-1f349578-9054-4db1-a9ac-5aa68505157c.png) + A video tutorial covering how to set up the guardrails for Github can be found here: https://www.youtube.com/watch?v=bbUNsjk6G7I The instructions above set out how to implement the Guardrail Examples for Github. We do however also provide support for other platforms: @@ -43,7 +47,11 @@ The instructions above set out how to implement the Guardrail Examples for Githu ## Workload Identity federation Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. -This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. +This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. + +The WIF strategy that we employ in our pipelining is to create environment branches for which we then map to service accounts. + +![Service Account Example](https://user-images.githubusercontent.com/94000358/224196168-bdab699d-4457-46b0-8e3a-68cfc1e9c3d7.png) If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw From b4867e94297fa01d5b380584b931c0709f249124 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:06:06 +0100 Subject: [PATCH 056/132] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8c8cff8..cf65c65 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ DevOps governance will give infrastructure teams the required flexibility whilst To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. We provide you with the following 3 different components: -![Guardrail Examples](https://user-images.githubusercontent.com/94000358/169811919-e5c36181-c1d2-4339-8103-d86640e9a1f1.png) +Guardrail Examples - The [Folder Factory](/examples/guardrails/github/folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. @@ -30,7 +30,7 @@ The Folder Factory and the Project Factory are usually maintained centrally (by This template is based on an "ideal" initial pipeline which is as follows: -![Ideal Pipeline Generic](https://user-images.githubusercontent.com/94000358/224195779-1f349578-9054-4db1-a9ac-5aa68505157c.png) +![Ideal Pipeline Generic](https://user-images.githubusercontent.com/94000358/224196745-4ce7e761-82d4-4eba-b0b2-2912ca73eccb.png) A video tutorial covering how to set up the guardrails for Github can be found here: https://www.youtube.com/watch?v=bbUNsjk6G7I From cb629e4745b3bf659364941f5f8fb996f8ba2cd2 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:09:24 +0100 Subject: [PATCH 057/132] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cf65c65..c2a5b69 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ DevOps governance will give infrastructure teams the required flexibility whilst To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. We provide you with the following 3 different components: -Guardrail Examples +Guardrail Examples - The [Folder Factory](/examples/guardrails/github/folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. From a4e4a966ffc395f7ab587ba71e786f22174d6bc7 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:10:02 +0100 Subject: [PATCH 058/132] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c2a5b69..2dc05d7 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ This template is based on an "ideal" initial pipeline which is as follows: A video tutorial covering how to set up the guardrails for Github can be found here: https://www.youtube.com/watch?v=bbUNsjk6G7I -The instructions above set out how to implement the Guardrail Examples for Github. We do however also provide support for other platforms: +The instructions above set out how to implement the Guardrail Examples for Github. We do however also provide support for other platforms. ## Supported Platforms From b0a791a31afdfc8fada69ae4802183fbd7428704 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:18:30 +0100 Subject: [PATCH 059/132] Added Bitbucket Readme --- examples/guardrails/bitbucket/README.md | 37 +++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 examples/guardrails/bitbucket/README.md diff --git a/examples/guardrails/bitbucket/README.md b/examples/guardrails/bitbucket/README.md new file mode 100644 index 0000000..6842271 --- /dev/null +++ b/examples/guardrails/bitbucket/README.md @@ -0,0 +1,37 @@ +# Bitbucket guardrail & pipeline example for individual workloads + +To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. We provide you with the following 3 different components: + +Guardrail Examples + +- The [Folder Factory](../bitbucket/folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. + +- The [Project Factory](../bitbucket/project-factory) sets up projects for teams. For this it creates a deployment service account, links this to a Github repository and defines the roles and permissions that the deployment service account has. + +The Folder Factory and the Project Factory are usually maintained centrally (by a cloud platform team) and used to manage the individual workloads. + +- The [Skunkworks - IaC Kickstarter](../bitbucket/skunkworks) is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. + +This template is based on an "ideal" initial pipeline which is as follows: + +![Ideal Pipeline Generic](https://user-images.githubusercontent.com/94000358/224196745-4ce7e761-82d4-4eba-b0b2-2912ca73eccb.png) + +A video tutorial covering how to set up the guardrails for Github can be found here: https://www.youtube.com/watch?v=bbUNsjk6G7I + +# Getting started + +## Workload Identity federation +Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. +This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +### High Level Process +* GCP + - Create a Workload Identity Pool + - Create a Workload Identity Provider + - Create a Service Account and grant permissions + +* CICD tool + - Specify where the pipeline configuration file resides + - Configure variables to pass relevant information to GCP to genrate short-lived tokens \ No newline at end of file From 33c12df8f648f0778548cb95fcfd84fcf4cfac75 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:19:58 +0100 Subject: [PATCH 060/132] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2dc05d7..e4e6e2e 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,12 @@ The instructions above set out how to implement the Guardrail Examples for Githu ## Supported Platforms - - [Bitbucket](/examples/guardrails/bitbucket/README.md) - - [Cloudbuild](/examples/guardrails/cloudbuild/README.md) - - [Github](/examples/guardrails/github/README.md) - - [Gitlab](/examples/guardrails/gitlab/README.md) - - [Jenkins](/examples/guardrails/jenkins/README.md) - - [Terraform-Cloud](/examples/guardrails/terraform-cloud/README.md) + - [Bitbucket](/examples/guardrails/bitbucket) + - [Cloudbuild](/examples/guardrails/cloudbuild) + - [Github](/examples/guardrails/github) + - [Gitlab](/examples/guardrails/gitlab) + - [Jenkins](/examples/guardrails/jenkins) + - [Terraform-Cloud](/examples/guardrails/terraform-cloud) ## Workload Identity federation Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. From a14f38332262160dfb33a8cb8d17dd82c3b3b781 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:27:20 +0100 Subject: [PATCH 061/132] Staging Readme Structure --- examples/guardrails/bitbucket/README.md | 6 +-- examples/guardrails/cloudbuild/README.md | 37 +++++++++++++++++++ examples/guardrails/github/README.md | 37 +++++++++++++++++++ examples/guardrails/gitlab/README.md | 22 ++++++++++- examples/guardrails/jenkins/README.md | 37 +++++++++++++++++++ examples/guardrails/terraform-cloud/README.md | 37 +++++++++++++++++++ 6 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 examples/guardrails/cloudbuild/README.md create mode 100644 examples/guardrails/github/README.md create mode 100644 examples/guardrails/jenkins/README.md create mode 100644 examples/guardrails/terraform-cloud/README.md diff --git a/examples/guardrails/bitbucket/README.md b/examples/guardrails/bitbucket/README.md index 6842271..66f1940 100644 --- a/examples/guardrails/bitbucket/README.md +++ b/examples/guardrails/bitbucket/README.md @@ -4,13 +4,13 @@ To demonstrate how to enforce guardrails and pipelines for Google Cloud we provi Guardrail Examples -- The [Folder Factory](../bitbucket/folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. +- The [Folder Factory](folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. -- The [Project Factory](../bitbucket/project-factory) sets up projects for teams. For this it creates a deployment service account, links this to a Github repository and defines the roles and permissions that the deployment service account has. +- The [Project Factory](project-factory) sets up projects for teams. For this it creates a deployment service account, links this to a Github repository and defines the roles and permissions that the deployment service account has. The Folder Factory and the Project Factory are usually maintained centrally (by a cloud platform team) and used to manage the individual workloads. -- The [Skunkworks - IaC Kickstarter](../bitbucket/skunkworks) is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. +- The [Skunkworks - IaC Kickstarter](skunkworks) is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. This template is based on an "ideal" initial pipeline which is as follows: diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md new file mode 100644 index 0000000..8ed34b5 --- /dev/null +++ b/examples/guardrails/cloudbuild/README.md @@ -0,0 +1,37 @@ +# Cloudbuild guardrail & pipeline example for individual workloads + +To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. We provide you with the following 3 different components: + +Guardrail Examples + +- The [Folder Factory](folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. + +- The [Project Factory](project-factory) sets up projects for teams. For this it creates a deployment service account, links this to a Github repository and defines the roles and permissions that the deployment service account has. + +The Folder Factory and the Project Factory are usually maintained centrally (by a cloud platform team) and used to manage the individual workloads. + +- The [Skunkworks - IaC Kickstarter](skunkworks) is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. + +This template is based on an "ideal" initial pipeline which is as follows: + +![Ideal Pipeline Generic](https://user-images.githubusercontent.com/94000358/224196745-4ce7e761-82d4-4eba-b0b2-2912ca73eccb.png) + +A video tutorial covering how to set up the guardrails for Github can be found here: https://www.youtube.com/watch?v=bbUNsjk6G7I + +# Getting started + +## Workload Identity federation +Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. +This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +### High Level Process +* GCP + - Create a Workload Identity Pool + - Create a Workload Identity Provider + - Create a Service Account and grant permissions + +* CICD tool + - Specify where the pipeline configuration file resides + - Configure variables to pass relevant information to GCP to genrate short-lived tokens \ No newline at end of file diff --git a/examples/guardrails/github/README.md b/examples/guardrails/github/README.md new file mode 100644 index 0000000..af7d41a --- /dev/null +++ b/examples/guardrails/github/README.md @@ -0,0 +1,37 @@ +# Github guardrail & pipeline example for individual workloads + +To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. We provide you with the following 3 different components: + +Guardrail Examples + +- The [Folder Factory](folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. + +- The [Project Factory](project-factory) sets up projects for teams. For this it creates a deployment service account, links this to a Github repository and defines the roles and permissions that the deployment service account has. + +The Folder Factory and the Project Factory are usually maintained centrally (by a cloud platform team) and used to manage the individual workloads. + +- The [Skunkworks - IaC Kickstarter](skunkworks) is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. + +This template is based on an "ideal" initial pipeline which is as follows: + +![Ideal Pipeline Generic](https://user-images.githubusercontent.com/94000358/224196745-4ce7e761-82d4-4eba-b0b2-2912ca73eccb.png) + +A video tutorial covering how to set up the guardrails for Github can be found here: https://www.youtube.com/watch?v=bbUNsjk6G7I + +# Getting started + +## Workload Identity federation +Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. +This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +### High Level Process +* GCP + - Create a Workload Identity Pool + - Create a Workload Identity Provider + - Create a Service Account and grant permissions + +* CICD tool + - Specify where the pipeline configuration file resides + - Configure variables to pass relevant information to GCP to genrate short-lived tokens \ No newline at end of file diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index ddd760d..70e217f 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -1,10 +1,30 @@ +# Gitlab guardrail & pipeline example for individual workloads + +To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. We provide you with the following 3 different components: + +Guardrail Examples + +- The [Folder Factory](folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. + +- The [Project Factory](project-factory) sets up projects for teams. For this it creates a deployment service account, links this to a Github repository and defines the roles and permissions that the deployment service account has. + +The Folder Factory and the Project Factory are usually maintained centrally (by a cloud platform team) and used to manage the individual workloads. + +- The [Skunkworks - IaC Kickstarter](skunkworks) is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. + +This template is based on an "ideal" initial pipeline which is as follows: + +![Ideal Pipeline Generic](https://user-images.githubusercontent.com/94000358/224196745-4ce7e761-82d4-4eba-b0b2-2912ca73eccb.png) + +A video tutorial covering how to set up the guardrails for Github can be found here: https://www.youtube.com/watch?v=bbUNsjk6G7I + +# Getting started This workflow covers the steps to setup gitlab CICD pipeline for terraform with gitlab SaaS shared runners. The setup involves setting up gitlab repository and the corresponding CI/CD settings and variables. The pipeline triggers based on select events (like push to specific branches), authenticates to the specified service account using Workload Identity federation and runs the pipeline to deploy infrastructure using terraform in GCP. Please look through the [README](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/README.md) for an overview of the CICD process. - ## Implementation Process The setup consists of configuring folder-factory, project-factory and skunkworks as three gitlab repositories. The pre-requisites and the setup are detailed below for each of the repositories. diff --git a/examples/guardrails/jenkins/README.md b/examples/guardrails/jenkins/README.md new file mode 100644 index 0000000..47b8e0a --- /dev/null +++ b/examples/guardrails/jenkins/README.md @@ -0,0 +1,37 @@ +# Jenkins guardrail & pipeline example for individual workloads + +To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. We provide you with the following 3 different components: + +Guardrail Examples + +- The [Folder Factory](folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. + +- The [Project Factory](project-factory) sets up projects for teams. For this it creates a deployment service account, links this to a Github repository and defines the roles and permissions that the deployment service account has. + +The Folder Factory and the Project Factory are usually maintained centrally (by a cloud platform team) and used to manage the individual workloads. + +- The [Skunkworks - IaC Kickstarter](skunkworks) is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. + +This template is based on an "ideal" initial pipeline which is as follows: + +![Ideal Pipeline Generic](https://user-images.githubusercontent.com/94000358/224196745-4ce7e761-82d4-4eba-b0b2-2912ca73eccb.png) + +A video tutorial covering how to set up the guardrails for Github can be found here: https://www.youtube.com/watch?v=bbUNsjk6G7I + +# Getting started + +## Workload Identity federation +Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. +This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +### High Level Process +* GCP + - Create a Workload Identity Pool + - Create a Workload Identity Provider + - Create a Service Account and grant permissions + +* CICD tool + - Specify where the pipeline configuration file resides + - Configure variables to pass relevant information to GCP to genrate short-lived tokens \ No newline at end of file diff --git a/examples/guardrails/terraform-cloud/README.md b/examples/guardrails/terraform-cloud/README.md new file mode 100644 index 0000000..b9fcf59 --- /dev/null +++ b/examples/guardrails/terraform-cloud/README.md @@ -0,0 +1,37 @@ +# Terraform Cloud guardrail & pipeline example for individual workloads + +To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. We provide you with the following 3 different components: + +Guardrail Examples + +- The [Folder Factory](folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. + +- The [Project Factory](project-factory) sets up projects for teams. For this it creates a deployment service account, links this to a Github repository and defines the roles and permissions that the deployment service account has. + +The Folder Factory and the Project Factory are usually maintained centrally (by a cloud platform team) and used to manage the individual workloads. + +- The [Skunkworks - IaC Kickstarter](skunkworks) is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. + +This template is based on an "ideal" initial pipeline which is as follows: + +![Ideal Pipeline Generic](https://user-images.githubusercontent.com/94000358/224196745-4ce7e761-82d4-4eba-b0b2-2912ca73eccb.png) + +A video tutorial covering how to set up the guardrails for Github can be found here: https://www.youtube.com/watch?v=bbUNsjk6G7I + +# Getting started + +## Workload Identity federation +Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. +This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. + +If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + +### High Level Process +* GCP + - Create a Workload Identity Pool + - Create a Workload Identity Provider + - Create a Service Account and grant permissions + +* CICD tool + - Specify where the pipeline configuration file resides + - Configure variables to pass relevant information to GCP to genrate short-lived tokens \ No newline at end of file From 143c97492c4ddd702834ec55fd6101dfee5cdd5a Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:28:59 +0100 Subject: [PATCH 062/132] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e4e6e2e..0a7897a 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,13 @@ To demonstrate how to enforce guardrails and pipelines for Google Cloud we provi Guardrail Examples -- The [Folder Factory](/examples/guardrails/github/folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. +- The *Folder Factory* creates folders and sets guardrails in the form of organisational policies on folders. -- The [Project Factory](/examples/guardrails/github/project-factory) sets up projects for teams. For this it creates a deployment service account, links this to a Github repository and defines the roles and permissions that the deployment service account has. +- The *Project Factory* sets up projects for teams. For this it creates a deployment service account, links this to a Github repository and defines the roles and permissions that the deployment service account has. The Folder Factory and the Project Factory are usually maintained centrally (by a cloud platform team) and used to manage the individual workloads. -- The [Skunkworks - IaC Kickstarter](/examples/guardrails/github/skunkworks) is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. +- The *Skunkworks - IaC Kickstarter* is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. This template is based on an "ideal" initial pipeline which is as follows: From 4dd86ad005f11141b051e3c59ef8e5dc17a01286 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:31:01 +0100 Subject: [PATCH 063/132] Update README.md --- examples/guardrails/bitbucket/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/bitbucket/README.md b/examples/guardrails/bitbucket/README.md index 66f1940..0df1adc 100644 --- a/examples/guardrails/bitbucket/README.md +++ b/examples/guardrails/bitbucket/README.md @@ -2,7 +2,7 @@ To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. We provide you with the following 3 different components: -Guardrail Examples +Bitbucket - The [Folder Factory](folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. @@ -34,4 +34,4 @@ If you do require additional assitance to setup Workload Identity Federation hav * CICD tool - Specify where the pipeline configuration file resides - - Configure variables to pass relevant information to GCP to genrate short-lived tokens \ No newline at end of file + - Configure variables to pass relevant information to GCP to genrate short-lived tokens From bddf98a5634bbb65b2b6f4691599010f72e7b00a Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:31:46 +0100 Subject: [PATCH 064/132] Update README.md --- examples/guardrails/github/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/github/README.md b/examples/guardrails/github/README.md index af7d41a..6dc1f9d 100644 --- a/examples/guardrails/github/README.md +++ b/examples/guardrails/github/README.md @@ -2,7 +2,7 @@ To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. We provide you with the following 3 different components: -Guardrail Examples +Github - The [Folder Factory](folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. @@ -34,4 +34,4 @@ If you do require additional assitance to setup Workload Identity Federation hav * CICD tool - Specify where the pipeline configuration file resides - - Configure variables to pass relevant information to GCP to genrate short-lived tokens \ No newline at end of file + - Configure variables to pass relevant information to GCP to genrate short-lived tokens From cbe8cdf8dc2ce3ba2d98b78a1ba74a942a8d8677 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:33:54 +0100 Subject: [PATCH 065/132] Update README.md --- examples/guardrails/gitlab/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index 70e217f..0e1b824 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -2,7 +2,7 @@ To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. We provide you with the following 3 different components: -Guardrail Examples +Gitlab - The [Folder Factory](folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. From f5e21b468a7e2023f5b6d6f0355590daf28fff39 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:35:31 +0100 Subject: [PATCH 066/132] Update README.md --- examples/guardrails/gitlab/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index 0e1b824..b6c97f3 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -14,7 +14,7 @@ The Folder Factory and the Project Factory are usually maintained centrally (by This template is based on an "ideal" initial pipeline which is as follows: -![Ideal Pipeline Generic](https://user-images.githubusercontent.com/94000358/224196745-4ce7e761-82d4-4eba-b0b2-2912ca73eccb.png) +![Ideal Pipeline Gitlab](https://user-images.githubusercontent.com/94000358/224200646-cf922479-4d80-4bb8-8df2-a397403df594.png) A video tutorial covering how to set up the guardrails for Github can be found here: https://www.youtube.com/watch?v=bbUNsjk6G7I From 22b5bb859a3d66ad5dbd963698df915e830995a1 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:36:53 +0100 Subject: [PATCH 067/132] Update README.md --- examples/guardrails/jenkins/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/jenkins/README.md b/examples/guardrails/jenkins/README.md index 47b8e0a..b539611 100644 --- a/examples/guardrails/jenkins/README.md +++ b/examples/guardrails/jenkins/README.md @@ -14,7 +14,7 @@ The Folder Factory and the Project Factory are usually maintained centrally (by This template is based on an "ideal" initial pipeline which is as follows: -![Ideal Pipeline Generic](https://user-images.githubusercontent.com/94000358/224196745-4ce7e761-82d4-4eba-b0b2-2912ca73eccb.png) +![Ideal Pipeline Jenkins](https://user-images.githubusercontent.com/94000358/224200805-f1e7c295-87b0-46a8-b048-e6c152b73930.png) A video tutorial covering how to set up the guardrails for Github can be found here: https://www.youtube.com/watch?v=bbUNsjk6G7I @@ -34,4 +34,4 @@ If you do require additional assitance to setup Workload Identity Federation hav * CICD tool - Specify where the pipeline configuration file resides - - Configure variables to pass relevant information to GCP to genrate short-lived tokens \ No newline at end of file + - Configure variables to pass relevant information to GCP to genrate short-lived tokens From c490ba3f4651400bd0dbf296afcbecf3192351f7 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:37:43 +0100 Subject: [PATCH 068/132] Update README.md --- examples/guardrails/github/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/github/README.md b/examples/guardrails/github/README.md index 6dc1f9d..5dfdad4 100644 --- a/examples/guardrails/github/README.md +++ b/examples/guardrails/github/README.md @@ -14,7 +14,7 @@ The Folder Factory and the Project Factory are usually maintained centrally (by This template is based on an "ideal" initial pipeline which is as follows: -![Ideal Pipeline Generic](https://user-images.githubusercontent.com/94000358/224196745-4ce7e761-82d4-4eba-b0b2-2912ca73eccb.png) +![Ideal Pipeline Github](https://user-images.githubusercontent.com/94000358/224200939-94df478c-cae5-41b3-bf0d-ed573da331f3.png) A video tutorial covering how to set up the guardrails for Github can be found here: https://www.youtube.com/watch?v=bbUNsjk6G7I From 853343abe478fea66382e4c4109bdc4ec147e010 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:38:41 +0100 Subject: [PATCH 069/132] Update README.md --- examples/guardrails/cloudbuild/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index 8ed34b5..63830c9 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -14,7 +14,7 @@ The Folder Factory and the Project Factory are usually maintained centrally (by This template is based on an "ideal" initial pipeline which is as follows: -![Ideal Pipeline Generic](https://user-images.githubusercontent.com/94000358/224196745-4ce7e761-82d4-4eba-b0b2-2912ca73eccb.png) +![Cloud Build](https://user-images.githubusercontent.com/94000358/224201056-3331e1d9-d833-43ba-b322-1b11387f033a.png) A video tutorial covering how to set up the guardrails for Github can be found here: https://www.youtube.com/watch?v=bbUNsjk6G7I @@ -34,4 +34,4 @@ If you do require additional assitance to setup Workload Identity Federation hav * CICD tool - Specify where the pipeline configuration file resides - - Configure variables to pass relevant information to GCP to genrate short-lived tokens \ No newline at end of file + - Configure variables to pass relevant information to GCP to genrate short-lived tokens From d48b43ae7315f86b3c53e0182762dd58cef16da1 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:39:37 +0100 Subject: [PATCH 070/132] Update README.md --- examples/guardrails/cloudbuild/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index 63830c9..1e98a0b 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -2,7 +2,7 @@ To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. We provide you with the following 3 different components: -Guardrail Examples +Github - The [Folder Factory](folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. From ea3caaa47e286badb7123e1e6c97be2d0b517b1c Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:43:30 +0100 Subject: [PATCH 071/132] Update README.md --- examples/guardrails/github/README.md | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/examples/guardrails/github/README.md b/examples/guardrails/github/README.md index 5dfdad4..5e051b4 100644 --- a/examples/guardrails/github/README.md +++ b/examples/guardrails/github/README.md @@ -20,18 +20,8 @@ A video tutorial covering how to set up the guardrails for Github can be found h # Getting started -## Workload Identity federation -Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. -This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. - -If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw - -### High Level Process -* GCP - - Create a Workload Identity Pool - - Create a Workload Identity Provider - - Create a Service Account and grant permissions - -* CICD tool - - Specify where the pipeline configuration file resides - - Configure variables to pass relevant information to GCP to genrate short-lived tokens +Deployment and configuration information can be found on the following pages: + +- [Folder Factory](folder-factory) +- [Project Factory](project-factory) +- [Skunkworks - IaC Kickstarter](skunkworks) From c4f3e5b374a9a60507dc312034640a0c92490801 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:44:56 +0100 Subject: [PATCH 072/132] Update README.md --- examples/guardrails/github/project-factory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/github/project-factory/README.md b/examples/guardrails/github/project-factory/README.md index f901e7f..150777e 100644 --- a/examples/guardrails/github/project-factory/README.md +++ b/examples/guardrails/github/project-factory/README.md @@ -2,7 +2,7 @@ This is a template for a DevOps project factory. -It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory) and is intended to house the projects of a specified folder: +It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/github/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/github/folder-factory) and is intended to house the projects of a specified folder: Overview From 489854ca08d853d77044e63b423b6ba27b90f664 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:03:17 +0100 Subject: [PATCH 073/132] Update README.md --- examples/guardrails/gitlab/folder-factory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/folder-factory/README.md b/examples/guardrails/gitlab/folder-factory/README.md index 9b724f7..9823a4b 100644 --- a/examples/guardrails/gitlab/folder-factory/README.md +++ b/examples/guardrails/gitlab/folder-factory/README.md @@ -4,7 +4,7 @@ This is a template for a DevOps folder factory. It can be used with [https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory](https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory) and is intended to house the folder configurations: -![Screenshot 2022-05-10 12 00 19 PM](https://user-images.githubusercontent.com/94000358/169809437-aaa8538e-3ffc-48b3-9028-84e4995de150.png) +Screenshot 2023-03-10 at 02 52 08 Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. From 73bd43f10daf8bb590bc8cf76d720560294ea3d0 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:04:34 +0100 Subject: [PATCH 074/132] Update README.md --- examples/guardrails/gitlab/project-factory/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/gitlab/project-factory/README.md b/examples/guardrails/gitlab/project-factory/README.md index 895021d..82aabf3 100644 --- a/examples/guardrails/gitlab/project-factory/README.md +++ b/examples/guardrails/gitlab/project-factory/README.md @@ -2,13 +2,13 @@ This is a template for a DevOps project factory. -It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory) and is intended to house the projects of a specified folder: +It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/gitlab/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/gitlab/folder-factory) and is intended to house the projects of a specified folder: Overview Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. -![Folder Factory](https://user-images.githubusercontent.com/94000358/169809882-f5ff9fb1-d037-49de-8c2c-bf0d457b662f.png) +Screenshot 2023-03-10 at 02 53 10 The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. From 1713a5269cc75281cb0151c366af4e1a42866424 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:05:18 +0100 Subject: [PATCH 075/132] Update README.md --- examples/guardrails/gitlab/project-factory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/project-factory/README.md b/examples/guardrails/gitlab/project-factory/README.md index 82aabf3..9f12e8c 100644 --- a/examples/guardrails/gitlab/project-factory/README.md +++ b/examples/guardrails/gitlab/project-factory/README.md @@ -46,7 +46,7 @@ roles: - roles/storage.objectAdmin - roles/storage.objectCreator - roles/storage.objectViewer -repo_provider: github +repo_provider: gitlab repo_name: devops-governance/skunkworks repo_branch: dev ``` From 8f98483c318e9c3f231eb598bd096d657854307d Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:07:43 +0100 Subject: [PATCH 076/132] Update README.md --- examples/guardrails/gitlab/skunkworks/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/gitlab/skunkworks/README.md b/examples/guardrails/gitlab/skunkworks/README.md index 2375940..04535e4 100644 --- a/examples/guardrails/gitlab/skunkworks/README.md +++ b/examples/guardrails/gitlab/skunkworks/README.md @@ -2,9 +2,11 @@ This is a template for an IaC kickstarter repository. -![Skunkworks](https://user-images.githubusercontent.com/94000358/169810982-36f01de2-e5e5-4ecd-b98e-3cf5a6aa9f81.png) +Screenshot 2023-03-10 at 02 53 38 -The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Gitlab. It is based on the following ideal pipeline: + +![Gitlab](https://user-images.githubusercontent.com/94000358/224205000-7cfb0fe0-6520-421b-88bd-ba7efb20ffd4.png) This template creates a bucket in the specified target environment. From dd6978c4b4abba42f19cef9817f0ad21042e168d Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:13:09 +0100 Subject: [PATCH 077/132] Update README.md --- examples/guardrails/github/skunkworks/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/github/skunkworks/README.md b/examples/guardrails/github/skunkworks/README.md index 0b89bb2..134d5c6 100644 --- a/examples/guardrails/github/skunkworks/README.md +++ b/examples/guardrails/github/skunkworks/README.md @@ -2,7 +2,7 @@ This is a template for an IaC kickstarter repository. -![Skunkworks](https://user-images.githubusercontent.com/94000358/169810982-36f01de2-e5e5-4ecd-b98e-3cf5a6aa9f81.png) +Screenshot 2023-03-10 at 03 09 49 The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. From 60e6ed0c8731f5bebc5aad9b0e4f74e9470cef2d Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:14:42 +0100 Subject: [PATCH 078/132] Update README.md --- examples/guardrails/github/folder-factory/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/github/folder-factory/README.md b/examples/guardrails/github/folder-factory/README.md index aa5036b..ee364f8 100644 --- a/examples/guardrails/github/folder-factory/README.md +++ b/examples/guardrails/github/folder-factory/README.md @@ -2,9 +2,9 @@ This is a template for a DevOps folder factory. -It can be used with [https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory](https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory) and is intended to house the folder configurations: +It can be used with [https://github.com/google/devops-governance/tree/main/examples/guardrails/github/project-factory](https://github.com/google/devops-governance/tree/main/examples/guardrails/github/project-factory) and is intended to house the folder configurations: -![Screenshot 2022-05-10 12 00 19 PM](https://user-images.githubusercontent.com/94000358/169809437-aaa8538e-3ffc-48b3-9028-84e4995de150.png) +Screenshot 2023-03-10 at 03 08 41 Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. From e08ad1cf636429290e70a6b5dacd6b7aad3b3396 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:15:58 +0100 Subject: [PATCH 079/132] Update README.md --- examples/guardrails/github/project-factory/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/github/project-factory/README.md b/examples/guardrails/github/project-factory/README.md index 150777e..026fd2e 100644 --- a/examples/guardrails/github/project-factory/README.md +++ b/examples/guardrails/github/project-factory/README.md @@ -6,9 +6,9 @@ It can be used with https://github.com/google/devops-governance/tree/main/exampl Overview -Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. +Using Keyless Authentication, the project factory connects a defined Github repository with a target service account and project within GCP for IaC. -![Folder Factory](https://user-images.githubusercontent.com/94000358/169809882-f5ff9fb1-d037-49de-8c2c-bf0d457b662f.png) +Screenshot 2023-03-10 at 03 09 10 The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. From cf36f52e05a7978bfd374245ca82441ca8f0cb7d Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:17:56 +0100 Subject: [PATCH 080/132] Update README.md --- examples/guardrails/github/skunkworks/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/guardrails/github/skunkworks/README.md b/examples/guardrails/github/skunkworks/README.md index 134d5c6..158532d 100644 --- a/examples/guardrails/github/skunkworks/README.md +++ b/examples/guardrails/github/skunkworks/README.md @@ -4,7 +4,9 @@ This is a template for an IaC kickstarter repository. Screenshot 2023-03-10 at 03 09 49 -The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. It is based on the following "ideal" pipeline: + +![Ideal Pipeline](https://user-images.githubusercontent.com/94000358/224206360-28c97f5d-603f-4f63-beeb-595bcee6039d.png) This template creates a bucket in the specified target environment. From fff2d1655f55b8fe00c3e05a934d1b3936520405 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:18:34 +0100 Subject: [PATCH 081/132] Update README.md --- examples/guardrails/gitlab/skunkworks/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/skunkworks/README.md b/examples/guardrails/gitlab/skunkworks/README.md index 04535e4..0b16bde 100644 --- a/examples/guardrails/gitlab/skunkworks/README.md +++ b/examples/guardrails/gitlab/skunkworks/README.md @@ -4,7 +4,7 @@ This is a template for an IaC kickstarter repository. Screenshot 2023-03-10 at 02 53 38 -The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Gitlab. It is based on the following ideal pipeline: +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Gitlab. It is based on the following "ideal" pipeline: ![Gitlab](https://user-images.githubusercontent.com/94000358/224205000-7cfb0fe0-6520-421b-88bd-ba7efb20ffd4.png) From 7a69901a863ef14dbeae02542004ba998ae33dd6 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:19:51 +0100 Subject: [PATCH 082/132] Update README.md --- examples/guardrails/bitbucket/folder-factory/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/bitbucket/folder-factory/README.md b/examples/guardrails/bitbucket/folder-factory/README.md index aa5036b..c7d3df8 100644 --- a/examples/guardrails/bitbucket/folder-factory/README.md +++ b/examples/guardrails/bitbucket/folder-factory/README.md @@ -2,9 +2,9 @@ This is a template for a DevOps folder factory. -It can be used with [https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory](https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory) and is intended to house the folder configurations: +It can be used with [https://github.com/google/devops-governance/tree/main/examples/guardrails/bitbucket/project-factory](https://github.com/google/devops-governance/tree/main/examples/guardrails/bitbucket/project-factory) and is intended to house the folder configurations: -![Screenshot 2022-05-10 12 00 19 PM](https://user-images.githubusercontent.com/94000358/169809437-aaa8538e-3ffc-48b3-9028-84e4995de150.png) +Screenshot 2023-03-10 at 03 11 31 Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. From 342c6b20f0923222776703afc58a36b78cd6b0d7 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:20:49 +0100 Subject: [PATCH 083/132] Update README.md --- examples/guardrails/bitbucket/project-factory/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/bitbucket/project-factory/README.md b/examples/guardrails/bitbucket/project-factory/README.md index f901e7f..34a9ab6 100644 --- a/examples/guardrails/bitbucket/project-factory/README.md +++ b/examples/guardrails/bitbucket/project-factory/README.md @@ -2,13 +2,13 @@ This is a template for a DevOps project factory. -It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory) and is intended to house the projects of a specified folder: +It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/bitbucket/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/bitbucket/folder-factory) and is intended to house the projects of a specified folder: Overview Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. -![Folder Factory](https://user-images.githubusercontent.com/94000358/169809882-f5ff9fb1-d037-49de-8c2c-bf0d457b662f.png) +Screenshot 2023-03-10 at 03 11 54 The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. From 7989bbe025e515442ef63af1b5f5bdb0407ed42e Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:21:49 +0100 Subject: [PATCH 084/132] Update README.md --- examples/guardrails/bitbucket/skunkworks/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/bitbucket/skunkworks/README.md b/examples/guardrails/bitbucket/skunkworks/README.md index 0b89bb2..4a2c84b 100644 --- a/examples/guardrails/bitbucket/skunkworks/README.md +++ b/examples/guardrails/bitbucket/skunkworks/README.md @@ -2,7 +2,7 @@ This is a template for an IaC kickstarter repository. -![Skunkworks](https://user-images.githubusercontent.com/94000358/169810982-36f01de2-e5e5-4ecd-b98e-3cf5a6aa9f81.png) +Screenshot 2023-03-10 at 03 12 19 The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. From 89028b413abc9f04186c18c570958ee9b7180c0f Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:25:00 +0100 Subject: [PATCH 085/132] Update README.md --- examples/guardrails/cloudbuild/project-factory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/cloudbuild/project-factory/README.md b/examples/guardrails/cloudbuild/project-factory/README.md index f901e7f..ce96a31 100644 --- a/examples/guardrails/cloudbuild/project-factory/README.md +++ b/examples/guardrails/cloudbuild/project-factory/README.md @@ -8,7 +8,7 @@ It can be used with https://github.com/google/devops-governance/tree/main/exampl Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. -![Folder Factory](https://user-images.githubusercontent.com/94000358/169809882-f5ff9fb1-d037-49de-8c2c-bf0d457b662f.png) +Screenshot 2023-03-10 at 03 09 10 The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. From a318589886b46aa29c7c1d29801fe6eae69444c1 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:26:28 +0100 Subject: [PATCH 086/132] Update README.md --- examples/guardrails/cloudbuild/skunkworks/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/cloudbuild/skunkworks/README.md b/examples/guardrails/cloudbuild/skunkworks/README.md index 0b89bb2..4e50db5 100644 --- a/examples/guardrails/cloudbuild/skunkworks/README.md +++ b/examples/guardrails/cloudbuild/skunkworks/README.md @@ -2,9 +2,11 @@ This is a template for an IaC kickstarter repository. -![Skunkworks](https://user-images.githubusercontent.com/94000358/169810982-36f01de2-e5e5-4ecd-b98e-3cf5a6aa9f81.png) +Screenshot 2023-03-10 at 03 09 49 -The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. It is based on the following "ideal" pipeline: + +![CloudBuild Pipeline](https://user-images.githubusercontent.com/94000358/224207506-8f61de84-6039-48eb-be36-59e68f6f5ef2.png) This template creates a bucket in the specified target environment. From bbdb01032c42568aef6d21145feafcd6056308d2 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:27:34 +0100 Subject: [PATCH 087/132] Update README.md --- examples/guardrails/jenkins/folder-factory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/jenkins/folder-factory/README.md b/examples/guardrails/jenkins/folder-factory/README.md index aa5036b..05b739c 100644 --- a/examples/guardrails/jenkins/folder-factory/README.md +++ b/examples/guardrails/jenkins/folder-factory/README.md @@ -4,7 +4,7 @@ This is a template for a DevOps folder factory. It can be used with [https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory](https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory) and is intended to house the folder configurations: -![Screenshot 2022-05-10 12 00 19 PM](https://user-images.githubusercontent.com/94000358/169809437-aaa8538e-3ffc-48b3-9028-84e4995de150.png) +Screenshot 2023-03-10 at 03 10 19 Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. From 1c3548a7f00b5356309285259ab010a6fb2a364e Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:28:08 +0100 Subject: [PATCH 088/132] Update README.md --- examples/guardrails/jenkins/project-factory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/jenkins/project-factory/README.md b/examples/guardrails/jenkins/project-factory/README.md index f901e7f..4fe5630 100644 --- a/examples/guardrails/jenkins/project-factory/README.md +++ b/examples/guardrails/jenkins/project-factory/README.md @@ -8,7 +8,7 @@ It can be used with https://github.com/google/devops-governance/tree/main/exampl Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. -![Folder Factory](https://user-images.githubusercontent.com/94000358/169809882-f5ff9fb1-d037-49de-8c2c-bf0d457b662f.png) +Screenshot 2023-03-10 at 03 10 46 The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. From baedae412bc7ae29ea6d00eeeb1c60515ee0a315 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:28:36 +0100 Subject: [PATCH 089/132] Update README.md --- examples/guardrails/jenkins/skunkworks/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/jenkins/skunkworks/README.md b/examples/guardrails/jenkins/skunkworks/README.md index 0b89bb2..0304c23 100644 --- a/examples/guardrails/jenkins/skunkworks/README.md +++ b/examples/guardrails/jenkins/skunkworks/README.md @@ -2,7 +2,7 @@ This is a template for an IaC kickstarter repository. -![Skunkworks](https://user-images.githubusercontent.com/94000358/169810982-36f01de2-e5e5-4ecd-b98e-3cf5a6aa9f81.png) +Screenshot 2023-03-10 at 03 11 13 The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. From 697bc72c6e5bffdd059f66243284d3160cbbca09 Mon Sep 17 00:00:00 2001 From: Alexander Meissner <94000358+amgoogle@users.noreply.github.com> Date: Fri, 10 Mar 2023 03:37:17 +0100 Subject: [PATCH 090/132] Update README.md --- examples/guardrails/cloudbuild/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index 1e98a0b..daa04cd 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -2,7 +2,7 @@ To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. We provide you with the following 3 different components: -Github +Screenshot 2023-03-10 at 02 08 05 - The [Folder Factory](folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. From 475472af3c852d2d3843b023cf0a944a873a894f Mon Sep 17 00:00:00 2001 From: Kodanda Rama Date: Fri, 10 Mar 2023 15:38:56 +0530 Subject: [PATCH 091/132] include README for policy validate --- examples/guardrails/gitlab/folder-factory/README.md | 4 ++++ examples/guardrails/gitlab/project-factory/README.md | 3 +++ examples/guardrails/gitlab/skunkworks/README.md | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/examples/guardrails/gitlab/folder-factory/README.md b/examples/guardrails/gitlab/folder-factory/README.md index 926eeba..7f0b98f 100644 --- a/examples/guardrails/gitlab/folder-factory/README.md +++ b/examples/guardrails/gitlab/folder-factory/README.md @@ -65,6 +65,10 @@ From the folder-factory Gitlab project page Add the variables to the pipeline as described in the table below. The same can be accessed from the README.md file under .gitlab/workflows in folder-factory. +### Terraform config validator +The pipeline has an option to utilise the integrated config validator (gcloud terraform vet) to impose constraints on your terraform configuration. You can enable it by setting the CI/CD Variable $TERRAFORM_POLICY_VALIDATE to "true" and providing the policy-library repo URL to $POLICY_LIBRARY_REPO variable. See the below for details on the Variables to be set on the CI/CD pipeline. + + | Variable | Description |Sample value | |--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| | GCP_PROJECT_ID | The GCP project ID of your service account | sample-project-1122 | diff --git a/examples/guardrails/gitlab/project-factory/README.md b/examples/guardrails/gitlab/project-factory/README.md index a53e9d3..82b1c62 100644 --- a/examples/guardrails/gitlab/project-factory/README.md +++ b/examples/guardrails/gitlab/project-factory/README.md @@ -74,6 +74,9 @@ CI/CD variables Navigate to Settings > CICD > expand Variables Add the variables to the pipeline as described in the table below. The same can be accessed from the README.md file under .gitlab/workflows in project-factory. +### Terraform config validator +The pipeline has an option to utilise the integrated config validator (gcloud terraform vet) to impose constraints on your terraform configuration. You can enable it by setting the CI/CD Variable $TERRAFORM_POLICY_VALIDATE to "true" and providing the policy-library repo URL to $POLICY_LIBRARY_REPO variable. See the below for details on the Variables to be set on the CI/CD pipeline. + | Variable | Description | Sample value | |--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| | GCP_PROJECT_ID | The GCP project ID of your service account | sample-project-1122 | diff --git a/examples/guardrails/gitlab/skunkworks/README.md b/examples/guardrails/gitlab/skunkworks/README.md index 10a4bce..433a052 100644 --- a/examples/guardrails/gitlab/skunkworks/README.md +++ b/examples/guardrails/gitlab/skunkworks/README.md @@ -26,6 +26,10 @@ The branch structure should mirror the environments that are going to be deploye * From the skunkworks project page, Navigate to Settings > CICD > expand Variables * Add the below variables to the pipeline +### Terraform config validator +The pipeline has an option to utilise the integrated config validator (gcloud terraform vet) to impose constraints on your terraform configuration. You can enable it by setting the CI/CD Variable $TERRAFORM_POLICY_VALIDATE to "true" and providing the policy-library repo URL to $POLICY_LIBRARY_REPO variable. See the below for details on the Variables to be set on the CI/CD pipeline. + + | Variable | Description | Sample value | |--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| | DEV_GCP_PROJECT_ID | The GCP project ID in which resources are to be created on a push event to dev branch | sample-dev-project-1122 | From 647f5f8babd5ce3b1dcfc547c41f0c5f32917ce4 Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 06:36:03 -0600 Subject: [PATCH 092/132] Update README.md --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0a7897a..f33ddef 100644 --- a/README.md +++ b/README.md @@ -36,14 +36,6 @@ A video tutorial covering how to set up the guardrails for Github can be found h The instructions above set out how to implement the Guardrail Examples for Github. We do however also provide support for other platforms. -## Supported Platforms - - - [Bitbucket](/examples/guardrails/bitbucket) - - [Cloudbuild](/examples/guardrails/cloudbuild) - - [Github](/examples/guardrails/github) - - [Gitlab](/examples/guardrails/gitlab) - - [Jenkins](/examples/guardrails/jenkins) - - [Terraform-Cloud](/examples/guardrails/terraform-cloud) ## Workload Identity federation Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. @@ -65,8 +57,17 @@ If you do require additional assitance to setup Workload Identity Federation hav - Specify where the pipeline configuration file resides - Configure variables to pass relevant information to GCP to genrate short-lived tokens -examples/guardrails section covers different CICD tools and how to leverage Workload Identity Federation between each tool and Google Cloud. +[examples/guardrails](/examples/guardrails) section covers different CICD tools and how to leverage Workload Identity Federation between each tool and Google Cloud. +## Supported Platforms + + - [Bitbucket](/examples/guardrails/bitbucket) + - [Cloudbuild](/examples/guardrails/cloudbuild) + - [Github](/examples/guardrails/github) + - [Gitlab](/examples/guardrails/gitlab) + - [Jenkins](/examples/guardrails/jenkins) + - [Terraform-Cloud](/examples/guardrails/terraform-cloud) + ## Disclaimer This is not an officially supported Google product. From 1940199c4fbcd7f09033d7e1d024c4b6fb1b481a Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 06:43:05 -0600 Subject: [PATCH 093/132] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f33ddef..ed4a339 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,12 @@ A video tutorial covering how to set up the guardrails for Github can be found h The instructions above set out how to implement the Guardrail Examples for Github. We do however also provide support for other platforms. -## Workload Identity federation +## Workload Identity federation + +Traditionally, applications running outside Google Cloud (like CICD tools) can use service account keys to access Google Cloud resources. However, service account keys are powerful credentials, and can present a security risk if they are not managed correctly. + +With identity federation, you can use Identity and Access Management (IAM) to grant external identities IAM roles, including the ability to impersonate service accounts. This approach eliminates the maintenance and security burden associated with service account keys. + Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. From a90d3e377fb859034728c336cb9940d796ef2593 Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 06:44:35 -0600 Subject: [PATCH 094/132] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed4a339..44e3dcf 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ If you do require additional assitance to setup Workload Identity Federation hav - Specify where the pipeline configuration file resides - Configure variables to pass relevant information to GCP to genrate short-lived tokens -[examples/guardrails](/examples/guardrails) section covers different CICD tools and how to leverage Workload Identity Federation between each tool and Google Cloud. +[examples/guardrails](/examples/guardrails) section covers different CICD tools and how to leverage Workload Identity Federation. ## Supported Platforms From 1e394b4a201ec772ceb50f2d3b538d1842826998 Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 06:54:42 -0600 Subject: [PATCH 095/132] Update README.md --- examples/guardrails/gitlab/README.md | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index b6c97f3..8cd55e2 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -1,22 +1,4 @@ -# Gitlab guardrail & pipeline example for individual workloads - -To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. We provide you with the following 3 different components: - -Gitlab - -- The [Folder Factory](folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. - -- The [Project Factory](project-factory) sets up projects for teams. For this it creates a deployment service account, links this to a Github repository and defines the roles and permissions that the deployment service account has. - -The Folder Factory and the Project Factory are usually maintained centrally (by a cloud platform team) and used to manage the individual workloads. - -- The [Skunkworks - IaC Kickstarter](skunkworks) is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. - -This template is based on an "ideal" initial pipeline which is as follows: - -![Ideal Pipeline Gitlab](https://user-images.githubusercontent.com/94000358/224200646-cf922479-4d80-4bb8-8df2-a397403df594.png) - -A video tutorial covering how to set up the guardrails for Github can be found here: https://www.youtube.com/watch?v=bbUNsjk6G7I +> A more comprehensive description of DevOps & GitOps principles can be found at [DevOps README](./../README.md). The module adopts the same principles and details instructions for Gitlab. # Getting started From bd75b302102186d5800bb365707fd9ad4ee5024b Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 06:55:05 -0600 Subject: [PATCH 096/132] Update README.md --- examples/guardrails/gitlab/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index 8cd55e2..6eb640c 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -1,4 +1,4 @@ -> A more comprehensive description of DevOps & GitOps principles can be found at [DevOps README](./../README.md). The module adopts the same principles and details instructions for Gitlab. +> A more comprehensive description of DevOps & GitOps principles can be found at [DevOps README](./../../../README.md). The module adopts the same principles and details instructions for Gitlab. # Getting started From 5413cc00ff1dec4b878c9b8e6c0877f54e9d9bb3 Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 06:56:34 -0600 Subject: [PATCH 097/132] Update README.md --- examples/guardrails/gitlab/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/gitlab/README.md b/examples/guardrails/gitlab/README.md index 6eb640c..93c4948 100644 --- a/examples/guardrails/gitlab/README.md +++ b/examples/guardrails/gitlab/README.md @@ -1,11 +1,11 @@ -> A more comprehensive description of DevOps & GitOps principles can be found at [DevOps README](./../../../README.md). The module adopts the same principles and details instructions for Gitlab. # Getting started This workflow covers the steps to setup gitlab CICD pipeline for terraform with gitlab SaaS shared runners. The setup involves setting up gitlab repository and the corresponding CI/CD settings and variables. The pipeline triggers based on select events (like push to specific branches), authenticates to the specified service account using Workload Identity federation and runs the pipeline to deploy infrastructure using terraform in GCP. -Please look through the [README](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/README.md) for an overview of the CICD process. + +> A more comprehensive description of DevOps & GitOps principles can be found at [DevOps README](./../../../README.md). ## Implementation Process From 4e8adcdfaeebaf3555fd34a1ba289bac45d710fc Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 08:16:34 -0600 Subject: [PATCH 098/132] Update README.md --- examples/guardrails/cloudbuild/README.md | 39 ++++++------------------ 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index daa04cd..b7dfbcc 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -1,37 +1,16 @@ # Cloudbuild guardrail & pipeline example for individual workloads -To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. We provide you with the following 3 different components: +To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. -Screenshot 2023-03-10 at 02 08 05 +> A more comprehensive description of DevOps & GitOps principles can be found at DevOps README. -- The [Folder Factory](folder-factory) creates folders and sets guardrails in the form of organisational policies on folders. -- The [Project Factory](project-factory) sets up projects for teams. For this it creates a deployment service account, links this to a Github repository and defines the roles and permissions that the deployment service account has. +### Implementation Process +Gitlab repository can be build with Cloud build by webhook triggers. Following are the steps to setup webhooks triggers to build repository from gitlab: -The Folder Factory and the Project Factory are usually maintained centrally (by a cloud platform team) and used to manage the individual workloads. +### Prerequisites +Enable the Cloud Build and secret manager API +Create a GCS bucket to store the terraform state file. Note: The bucket name will be configured as a cloud build variable substitutions while creating the cloud build trigger. +Gitlab repository with Folder Factory, Project Factory and Skunkworks Terraform code. +Note: SSH access on the gitlab repository should be given to allow users on the server to add their own ssh keys and use those ssh keys to secure git operations between their computer and gitlab instance. -- The [Skunkworks - IaC Kickstarter](skunkworks) is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. - -This template is based on an "ideal" initial pipeline which is as follows: - -![Cloud Build](https://user-images.githubusercontent.com/94000358/224201056-3331e1d9-d833-43ba-b322-1b11387f033a.png) - -A video tutorial covering how to set up the guardrails for Github can be found here: https://www.youtube.com/watch?v=bbUNsjk6G7I - -# Getting started - -## Workload Identity federation -Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. -This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. - -If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw - -### High Level Process -* GCP - - Create a Workload Identity Pool - - Create a Workload Identity Provider - - Create a Service Account and grant permissions - -* CICD tool - - Specify where the pipeline configuration file resides - - Configure variables to pass relevant information to GCP to genrate short-lived tokens From c3a39b2eff4c758abb0d263724b198773b29c3e7 Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 08:20:37 -0600 Subject: [PATCH 099/132] Update README.md --- examples/guardrails/cloudbuild/README.md | 29 +++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index b7dfbcc..e2388ee 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -2,7 +2,7 @@ To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. -> A more comprehensive description of DevOps & GitOps principles can be found at DevOps README. +> A more comprehensive description of DevOps & GitOps principles can be found at [DevOps README](./../../../README.md). ### Implementation Process @@ -14,3 +14,30 @@ Create a GCS bucket to store the terraform state file. Note: The bucket name wil Gitlab repository with Folder Factory, Project Factory and Skunkworks Terraform code. Note: SSH access on the gitlab repository should be given to allow users on the server to add their own ssh keys and use those ssh keys to secure git operations between their computer and gitlab instance. +### Setup SSH Keys + +1. Follow the steps here to complete the ssh key setup and cloud build trigger creation and Gitlab webhook setup, which involves following steps: +* Follow the steps in this document to create ssh keys. +- The ssh keys are required to access the gitlab repository code, these ssh keys are added in the GCP secret manager, which is then retrieved in the cloud build inline configuration to clone the gitlab repository. +Add your public ssh access keys on gitlab. +This step allows the ssh keys access to clone the gitlab repository in cloud build jobs. +Add ssh key credentials in Secret Manager. +These ssh keys have access to clone the gitlab repository. Note: The cloud build trigger created in the following steps should have the Secret Manager Secret Accessor IAM role on the service account assigned to the cloud build trigger. +Create a webhook trigger from the GCP console. +This webhook trigger is responsible for running the CICD job to deploy the terraform code. Note: This step also involves creating a GCP secret version which is different from the secret created in step 1.c. This secret is used by the cloud build webhook URL to send webhook events from gitlab. +Create a webhook in Gitlab +Using the webhook URL generated on cloudbuild side, configure the hook on Gitlab. +2. (Optional) Use the test feature on the Gitlab webhook section to make sure changes on Gitlab send a trigger to cloudbuild. +The cloud build webhook created with sample cloud build inline configuration can be triggered to validate the working of the webhook trigger. Navigate to Cloudbuild > Settings > Webhooks page to send the webhook trigger event by clicking the push event(or the event for which the gitlab trigger is configured): + +IMAGE + +This will invoke the cloud build job in the GCP console. + +3. Update the inline cloud build config for cloud build trigger created in step #5 with respective cloudbuild CICD file for the project. I.e. +Folder Factory cloud build inline build config. +Project Factory cloud build inline build config. +Skunkworks cloud build inline build config. The skunkworks cloud build config files are created for following environments: +Development: Note: This config file is implemented for the development environment and will be only deployed if the cloud build is triggered from the “dev” branch, this can be updated by updating the “If condition” in “Terraform Apply” Stage. +Staging: Note: This config file is implemented for the staging environment and will be only deployed if the cloud build is triggered from the “staging” branch, this can be updated by updating the “If condition” in “Terraform Apply” Stage. +Production: Note: This config file is implemented for the production environment and will be only deployed if the cloud build is triggered from the “main” branch, this can be updated by updating the “If condition” in “Terraform Apply” Stage. From 52fb33bb5d3a5d8b23471b83da58d58db7da147b Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 08:21:18 -0600 Subject: [PATCH 100/132] Update README.md --- examples/guardrails/cloudbuild/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index e2388ee..d9e2eb4 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -19,14 +19,14 @@ Note: SSH access on the gitlab repository should be given to allow users on the 1. Follow the steps here to complete the ssh key setup and cloud build trigger creation and Gitlab webhook setup, which involves following steps: * Follow the steps in this document to create ssh keys. - The ssh keys are required to access the gitlab repository code, these ssh keys are added in the GCP secret manager, which is then retrieved in the cloud build inline configuration to clone the gitlab repository. -Add your public ssh access keys on gitlab. -This step allows the ssh keys access to clone the gitlab repository in cloud build jobs. -Add ssh key credentials in Secret Manager. -These ssh keys have access to clone the gitlab repository. Note: The cloud build trigger created in the following steps should have the Secret Manager Secret Accessor IAM role on the service account assigned to the cloud build trigger. -Create a webhook trigger from the GCP console. -This webhook trigger is responsible for running the CICD job to deploy the terraform code. Note: This step also involves creating a GCP secret version which is different from the secret created in step 1.c. This secret is used by the cloud build webhook URL to send webhook events from gitlab. -Create a webhook in Gitlab -Using the webhook URL generated on cloudbuild side, configure the hook on Gitlab. +* Add your public ssh access keys on gitlab. +- This step allows the ssh keys access to clone the gitlab repository in cloud build jobs. +* Add ssh key credentials in Secret Manager. +- These ssh keys have access to clone the gitlab repository. Note: The cloud build trigger created in the following steps should have the Secret Manager Secret Accessor IAM role on the service account assigned to the cloud build trigger. +* Create a webhook trigger from the GCP console. +- This webhook trigger is responsible for running the CICD job to deploy the terraform code. Note: This step also involves creating a GCP secret version which is different from the secret created in step 1.c. This secret is used by the cloud build webhook URL to send webhook events from gitlab. +* Create a webhook in Gitlab +- Using the webhook URL generated on cloudbuild side, configure the hook on Gitlab. 2. (Optional) Use the test feature on the Gitlab webhook section to make sure changes on Gitlab send a trigger to cloudbuild. The cloud build webhook created with sample cloud build inline configuration can be triggered to validate the working of the webhook trigger. Navigate to Cloudbuild > Settings > Webhooks page to send the webhook trigger event by clicking the push event(or the event for which the gitlab trigger is configured): From aa578b6bdea4e9ed76ab5c07bef5ce5927519ef6 Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 08:21:38 -0600 Subject: [PATCH 101/132] Update README.md --- examples/guardrails/cloudbuild/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index d9e2eb4..331057b 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -18,7 +18,7 @@ Note: SSH access on the gitlab repository should be given to allow users on the 1. Follow the steps here to complete the ssh key setup and cloud build trigger creation and Gitlab webhook setup, which involves following steps: * Follow the steps in this document to create ssh keys. -- The ssh keys are required to access the gitlab repository code, these ssh keys are added in the GCP secret manager, which is then retrieved in the cloud build inline configuration to clone the gitlab repository. +- The ssh keys are required to access the gitlab repository code, these ssh keys are added in the GCP secret manager, which is then retrieved in the cloud build inline configuration to clone the gitlab repository. * Add your public ssh access keys on gitlab. - This step allows the ssh keys access to clone the gitlab repository in cloud build jobs. * Add ssh key credentials in Secret Manager. From 3d85a24999548d817f074596f8acdfc868f25014 Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 08:22:21 -0600 Subject: [PATCH 102/132] Update README.md --- examples/guardrails/cloudbuild/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index 331057b..e901b2f 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -18,7 +18,7 @@ Note: SSH access on the gitlab repository should be given to allow users on the 1. Follow the steps here to complete the ssh key setup and cloud build trigger creation and Gitlab webhook setup, which involves following steps: * Follow the steps in this document to create ssh keys. -- The ssh keys are required to access the gitlab repository code, these ssh keys are added in the GCP secret manager, which is then retrieved in the cloud build inline configuration to clone the gitlab repository. + * The ssh keys are required to access the gitlab repository code, these ssh keys are added in the GCP secret manager, which is then retrieved in the cloud build inline configuration to clone the gitlab repository. * Add your public ssh access keys on gitlab. - This step allows the ssh keys access to clone the gitlab repository in cloud build jobs. * Add ssh key credentials in Secret Manager. From 56883210edc00eb9048b21ebe44f4242f3df4eb3 Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 08:22:55 -0600 Subject: [PATCH 103/132] Update README.md --- examples/guardrails/cloudbuild/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index e901b2f..0b1e928 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -20,13 +20,13 @@ Note: SSH access on the gitlab repository should be given to allow users on the * Follow the steps in this document to create ssh keys. * The ssh keys are required to access the gitlab repository code, these ssh keys are added in the GCP secret manager, which is then retrieved in the cloud build inline configuration to clone the gitlab repository. * Add your public ssh access keys on gitlab. -- This step allows the ssh keys access to clone the gitlab repository in cloud build jobs. + * This step allows the ssh keys access to clone the gitlab repository in cloud build jobs. * Add ssh key credentials in Secret Manager. -- These ssh keys have access to clone the gitlab repository. Note: The cloud build trigger created in the following steps should have the Secret Manager Secret Accessor IAM role on the service account assigned to the cloud build trigger. + * These ssh keys have access to clone the gitlab repository. Note: The cloud build trigger created in the following steps should have the Secret Manager Secret Accessor IAM role on the service account assigned to the cloud build trigger. * Create a webhook trigger from the GCP console. -- This webhook trigger is responsible for running the CICD job to deploy the terraform code. Note: This step also involves creating a GCP secret version which is different from the secret created in step 1.c. This secret is used by the cloud build webhook URL to send webhook events from gitlab. + * This webhook trigger is responsible for running the CICD job to deploy the terraform code. Note: This step also involves creating a GCP secret version which is different from the secret created in step 1.c. This secret is used by the cloud build webhook URL to send webhook events from gitlab. * Create a webhook in Gitlab -- Using the webhook URL generated on cloudbuild side, configure the hook on Gitlab. + * Using the webhook URL generated on cloudbuild side, configure the hook on Gitlab. 2. (Optional) Use the test feature on the Gitlab webhook section to make sure changes on Gitlab send a trigger to cloudbuild. The cloud build webhook created with sample cloud build inline configuration can be triggered to validate the working of the webhook trigger. Navigate to Cloudbuild > Settings > Webhooks page to send the webhook trigger event by clicking the push event(or the event for which the gitlab trigger is configured): From 31fcc88cc44aaab8abebb2bea54173f0c45868ab Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 08:23:15 -0600 Subject: [PATCH 104/132] Update README.md --- examples/guardrails/cloudbuild/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index 0b1e928..5fbe26b 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -20,13 +20,13 @@ Note: SSH access on the gitlab repository should be given to allow users on the * Follow the steps in this document to create ssh keys. * The ssh keys are required to access the gitlab repository code, these ssh keys are added in the GCP secret manager, which is then retrieved in the cloud build inline configuration to clone the gitlab repository. * Add your public ssh access keys on gitlab. - * This step allows the ssh keys access to clone the gitlab repository in cloud build jobs. + * This step allows the ssh keys access to clone the gitlab repository in cloud build jobs. * Add ssh key credentials in Secret Manager. - * These ssh keys have access to clone the gitlab repository. Note: The cloud build trigger created in the following steps should have the Secret Manager Secret Accessor IAM role on the service account assigned to the cloud build trigger. + * These ssh keys have access to clone the gitlab repository. Note: The cloud build trigger created in the following steps should have the Secret Manager Secret Accessor IAM role on the service account assigned to the cloud build trigger. * Create a webhook trigger from the GCP console. - * This webhook trigger is responsible for running the CICD job to deploy the terraform code. Note: This step also involves creating a GCP secret version which is different from the secret created in step 1.c. This secret is used by the cloud build webhook URL to send webhook events from gitlab. + * This webhook trigger is responsible for running the CICD job to deploy the terraform code. Note: This step also involves creating a GCP secret version which is different from the secret created in step 1.c. This secret is used by the cloud build webhook URL to send webhook events from gitlab. * Create a webhook in Gitlab - * Using the webhook URL generated on cloudbuild side, configure the hook on Gitlab. + * Using the webhook URL generated on cloudbuild side, configure the hook on Gitlab. 2. (Optional) Use the test feature on the Gitlab webhook section to make sure changes on Gitlab send a trigger to cloudbuild. The cloud build webhook created with sample cloud build inline configuration can be triggered to validate the working of the webhook trigger. Navigate to Cloudbuild > Settings > Webhooks page to send the webhook trigger event by clicking the push event(or the event for which the gitlab trigger is configured): From f814ed475f1e1d498d9c7d27c07fc56d061dc645 Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 08:23:33 -0600 Subject: [PATCH 105/132] Update README.md --- examples/guardrails/cloudbuild/README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index 5fbe26b..171aaa2 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -17,16 +17,16 @@ Note: SSH access on the gitlab repository should be given to allow users on the ### Setup SSH Keys 1. Follow the steps here to complete the ssh key setup and cloud build trigger creation and Gitlab webhook setup, which involves following steps: -* Follow the steps in this document to create ssh keys. - * The ssh keys are required to access the gitlab repository code, these ssh keys are added in the GCP secret manager, which is then retrieved in the cloud build inline configuration to clone the gitlab repository. -* Add your public ssh access keys on gitlab. - * This step allows the ssh keys access to clone the gitlab repository in cloud build jobs. -* Add ssh key credentials in Secret Manager. - * These ssh keys have access to clone the gitlab repository. Note: The cloud build trigger created in the following steps should have the Secret Manager Secret Accessor IAM role on the service account assigned to the cloud build trigger. -* Create a webhook trigger from the GCP console. - * This webhook trigger is responsible for running the CICD job to deploy the terraform code. Note: This step also involves creating a GCP secret version which is different from the secret created in step 1.c. This secret is used by the cloud build webhook URL to send webhook events from gitlab. -* Create a webhook in Gitlab - * Using the webhook URL generated on cloudbuild side, configure the hook on Gitlab. + * Follow the steps in this document to create ssh keys. + * The ssh keys are required to access the gitlab repository code, these ssh keys are added in the GCP secret manager, which is then retrieved in the cloud build inline configuration to clone the gitlab repository. + * Add your public ssh access keys on gitlab. + * This step allows the ssh keys access to clone the gitlab repository in cloud build jobs. + * Add ssh key credentials in Secret Manager. + * These ssh keys have access to clone the gitlab repository. Note: The cloud build trigger created in the following steps should have the Secret Manager Secret Accessor IAM role on the service account assigned to the cloud build trigger. + * Create a webhook trigger from the GCP console. + * This webhook trigger is responsible for running the CICD job to deploy the terraform code. Note: This step also involves creating a GCP secret version which is different from the secret created in step 1.c. This secret is used by the cloud build webhook URL to send webhook events from gitlab. + * Create a webhook in Gitlab + * Using the webhook URL generated on cloudbuild side, configure the hook on Gitlab. 2. (Optional) Use the test feature on the Gitlab webhook section to make sure changes on Gitlab send a trigger to cloudbuild. The cloud build webhook created with sample cloud build inline configuration can be triggered to validate the working of the webhook trigger. Navigate to Cloudbuild > Settings > Webhooks page to send the webhook trigger event by clicking the push event(or the event for which the gitlab trigger is configured): From 432afc040f36500ad26c4566618704bf13abdd80 Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 08:23:50 -0600 Subject: [PATCH 106/132] Update README.md --- examples/guardrails/cloudbuild/README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index 171aaa2..ac2c970 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -17,16 +17,16 @@ Note: SSH access on the gitlab repository should be given to allow users on the ### Setup SSH Keys 1. Follow the steps here to complete the ssh key setup and cloud build trigger creation and Gitlab webhook setup, which involves following steps: - * Follow the steps in this document to create ssh keys. - * The ssh keys are required to access the gitlab repository code, these ssh keys are added in the GCP secret manager, which is then retrieved in the cloud build inline configuration to clone the gitlab repository. - * Add your public ssh access keys on gitlab. - * This step allows the ssh keys access to clone the gitlab repository in cloud build jobs. - * Add ssh key credentials in Secret Manager. - * These ssh keys have access to clone the gitlab repository. Note: The cloud build trigger created in the following steps should have the Secret Manager Secret Accessor IAM role on the service account assigned to the cloud build trigger. - * Create a webhook trigger from the GCP console. - * This webhook trigger is responsible for running the CICD job to deploy the terraform code. Note: This step also involves creating a GCP secret version which is different from the secret created in step 1.c. This secret is used by the cloud build webhook URL to send webhook events from gitlab. - * Create a webhook in Gitlab - * Using the webhook URL generated on cloudbuild side, configure the hook on Gitlab. + * Follow the steps in this document to create ssh keys. + * The ssh keys are required to access the gitlab repository code, these ssh keys are added in the GCP secret manager, which is then retrieved in the cloud build inline configuration to clone the gitlab repository. + * Add your public ssh access keys on gitlab. + * This step allows the ssh keys access to clone the gitlab repository in cloud build jobs. + * Add ssh key credentials in Secret Manager. + * These ssh keys have access to clone the gitlab repository. Note: The cloud build trigger created in the following steps should have the Secret Manager Secret Accessor IAM role on the service account assigned to the cloud build trigger. + * Create a webhook trigger from the GCP console. + * This webhook trigger is responsible for running the CICD job to deploy the terraform code. Note: This step also involves creating a GCP secret version which is different from the secret created in step 1.c. This secret is used by the cloud build webhook URL to send webhook events from gitlab. + * Create a webhook in Gitlab + * Using the webhook URL generated on cloudbuild side, configure the hook on Gitlab. 2. (Optional) Use the test feature on the Gitlab webhook section to make sure changes on Gitlab send a trigger to cloudbuild. The cloud build webhook created with sample cloud build inline configuration can be triggered to validate the working of the webhook trigger. Navigate to Cloudbuild > Settings > Webhooks page to send the webhook trigger event by clicking the push event(or the event for which the gitlab trigger is configured): From da7824664f89eaebd5289688208fb8ae2232c793 Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 08:26:10 -0600 Subject: [PATCH 107/132] Update README.md --- examples/guardrails/cloudbuild/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index ac2c970..7aa2f1b 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -16,8 +16,8 @@ Note: SSH access on the gitlab repository should be given to allow users on the ### Setup SSH Keys -1. Follow the steps here to complete the ssh key setup and cloud build trigger creation and Gitlab webhook setup, which involves following steps: - * Follow the steps in this document to create ssh keys. +1. Follow the steps [here](https://cloud.google.com/build/docs/automating-builds/gitlab/build-repos-from-gitlab#setting_up) to complete the ssh key setup and cloud build trigger creation and Gitlab webhook setup, which involves following steps: + * Create ssh keys. * The ssh keys are required to access the gitlab repository code, these ssh keys are added in the GCP secret manager, which is then retrieved in the cloud build inline configuration to clone the gitlab repository. * Add your public ssh access keys on gitlab. * This step allows the ssh keys access to clone the gitlab repository in cloud build jobs. @@ -27,6 +27,7 @@ Note: SSH access on the gitlab repository should be given to allow users on the * This webhook trigger is responsible for running the CICD job to deploy the terraform code. Note: This step also involves creating a GCP secret version which is different from the secret created in step 1.c. This secret is used by the cloud build webhook URL to send webhook events from gitlab. * Create a webhook in Gitlab * Using the webhook URL generated on cloudbuild side, configure the hook on Gitlab. + 2. (Optional) Use the test feature on the Gitlab webhook section to make sure changes on Gitlab send a trigger to cloudbuild. The cloud build webhook created with sample cloud build inline configuration can be triggered to validate the working of the webhook trigger. Navigate to Cloudbuild > Settings > Webhooks page to send the webhook trigger event by clicking the push event(or the event for which the gitlab trigger is configured): From b16416eaca85ae36533e04be6b5a1f80c715424c Mon Sep 17 00:00:00 2001 From: agutta Date: Sat, 11 Mar 2023 08:26:59 -0600 Subject: [PATCH 108/132] Update README.md --- examples/guardrails/cloudbuild/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index 7aa2f1b..f0c0d4d 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -29,7 +29,8 @@ Note: SSH access on the gitlab repository should be given to allow users on the * Using the webhook URL generated on cloudbuild side, configure the hook on Gitlab. 2. (Optional) Use the test feature on the Gitlab webhook section to make sure changes on Gitlab send a trigger to cloudbuild. -The cloud build webhook created with sample cloud build inline configuration can be triggered to validate the working of the webhook trigger. Navigate to Cloudbuild > Settings > Webhooks page to send the webhook trigger event by clicking the push event(or the event for which the gitlab trigger is configured): + * The cloud build webhook created with sample cloud build inline configuration can be triggered to validate the working of the webhook trigger. + * Navigate to Cloudbuild > Settings > Webhooks page to send the webhook trigger event by clicking the push event(or the event for which the gitlab trigger is configured): IMAGE From 5da71aa88f10a17d885e2789e7f23b52dc6450f3 Mon Sep 17 00:00:00 2001 From: pawanphalak Date: Mon, 13 Mar 2023 10:11:45 +0530 Subject: [PATCH 109/132] Readme updates for cloudbuild. Created main readme file at cloudbuild directory --- examples/guardrails/cloudbuild/README.md | 70 +++++++++++--- .../workflows/terraform-deployment.yml | 91 ------------------ .../.gitlab/workflows/cloudbuild.yaml | 0 .../.jenkins/workflows/cloudbuild.yaml | 0 .../workflows/cloudbuild.yaml | 0 .../worflows/cloudbuild.yaml | 0 .../workflows/terraform-deployment.yml | 94 ------------------- .../.github/workflows/tf-actions-dev.yml | 91 ------------------ .../.github/workflows/tf-actions-prod.yml | 91 ------------------ .../.github/workflows/tf-actions-stage.yml | 91 ------------------ 10 files changed, 57 insertions(+), 471 deletions(-) delete mode 100644 examples/guardrails/cloudbuild/folder-factory/.github/workflows/terraform-deployment.yml delete mode 100644 examples/guardrails/cloudbuild/folder-factory/.gitlab/workflows/cloudbuild.yaml delete mode 100644 examples/guardrails/cloudbuild/folder-factory/.jenkins/workflows/cloudbuild.yaml delete mode 100644 examples/guardrails/cloudbuild/folder-factory/.terraform-cloud/workflows/cloudbuild.yaml delete mode 100644 examples/guardrails/cloudbuild/folder-factory/.terraform-enterprise/worflows/cloudbuild.yaml delete mode 100644 examples/guardrails/cloudbuild/project-factory/.github/workflows/terraform-deployment.yml delete mode 100644 examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-dev.yml delete mode 100644 examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-prod.yml delete mode 100644 examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-stage.yml diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index f0c0d4d..e7e6fe5 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -1,18 +1,18 @@ # Cloudbuild guardrail & pipeline example for individual workloads -To demonstrate how to enforce guardrails and pipelines for Google Cloud we provide the "Guardrail Examples". The purpose of these examples is demonstrate how to provision access & guardrails to new workloads with IaC. +This workflow covers the steps to setup webhooks in GCP Cloud Build and gitlab to build the repository in gitlab using GCP Cloud build runners. The gitlab with cloud build setup involves creating webhooks in cloud build and gitlab. The webhook is then triggered based on the selected events(e.g. Push event) to trigger Cloud Build pipeline. > A more comprehensive description of DevOps & GitOps principles can be found at [DevOps README](./../../../README.md). -### Implementation Process +## Implementation Process Gitlab repository can be build with Cloud build by webhook triggers. Following are the steps to setup webhooks triggers to build repository from gitlab: ### Prerequisites -Enable the Cloud Build and secret manager API -Create a GCS bucket to store the terraform state file. Note: The bucket name will be configured as a cloud build variable substitutions while creating the cloud build trigger. -Gitlab repository with Folder Factory, Project Factory and Skunkworks Terraform code. -Note: SSH access on the gitlab repository should be given to allow users on the server to add their own ssh keys and use those ssh keys to secure git operations between their computer and gitlab instance. +* Enable the Cloud Build and secret manager API +* Create a GCS bucket to store the terraform state file. Note: The bucket name will be configured as a cloud build variable substitutions while creating the cloud build trigger. +* Gitlab repository with Folder Factory, Project Factory and Skunkworks Terraform code. +* **Note**: SSH access on the gitlab repository should be given to allow users on the server to add their own ssh keys and use those ssh keys to secure git operations between their computer and gitlab instance. ### Setup SSH Keys @@ -34,12 +34,56 @@ Note: SSH access on the gitlab repository should be given to allow users on the IMAGE -This will invoke the cloud build job in the GCP console. + * This will invoke the cloud build job in the GCP console. 3. Update the inline cloud build config for cloud build trigger created in step #5 with respective cloudbuild CICD file for the project. I.e. -Folder Factory cloud build inline build config. -Project Factory cloud build inline build config. -Skunkworks cloud build inline build config. The skunkworks cloud build config files are created for following environments: -Development: Note: This config file is implemented for the development environment and will be only deployed if the cloud build is triggered from the “dev” branch, this can be updated by updating the “If condition” in “Terraform Apply” Stage. -Staging: Note: This config file is implemented for the staging environment and will be only deployed if the cloud build is triggered from the “staging” branch, this can be updated by updating the “If condition” in “Terraform Apply” Stage. -Production: Note: This config file is implemented for the production environment and will be only deployed if the cloud build is triggered from the “main” branch, this can be updated by updating the “If condition” in “Terraform Apply” Stage. + * [Folder Factory cloud build inline build config](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/examples/guardrails/cloudbuild/folder-factory/.cloudbuild/workflows/cloudbuild.yaml). + * [Project Factory cloud build inline build config.](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/examples/guardrails/cloudbuild/project-factory/.cloudbuild/workflows/cloudbuild.yaml) + * [Skunkworks cloud build inline build config.](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows) The skunkworks cloud build config files are created for following environments: + * [Development](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/tf-cloudbuild-dev.yaml): Note: This config file is implemented for the development environment and will be only deployed if the cloud build is triggered from the “dev” branch, this can be updated by updating the “If condition” in “Terraform Apply” Stage. + * [Staging](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/tf-cloudbuild-stage.yaml): Note: This config file is implemented for the staging environment and will be only deployed if the cloud build is triggered from the “staging” branch, this can be updated by updating the “If condition” in “Terraform Apply” Stage. + * [Production](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/examples/guardrails/cloudbuild/skunkworks/.cloudbuild/workflows/tf-cloudbuild-prod.yaml): Note: This config file is implemented for the production environment and will be only deployed if the cloud build is triggered from the “main” branch, this can be updated by updating the “If condition” in “Terraform Apply” Stage. + +### Update variable substitutions in webhook trigger + +* Following substitutions variables needs to be configured in cloud build trigger to complete cloud build trigger setup: + +| KEY | VALUE |DESCRIPTION | REQUIRED | +|-----|-------|------------|----------| +| _BRANCH | $(body.ref) | This variable provides information about the branch which invoked the trigger. | YES | +| _SECRET | E.g. projects/123456789/secrets/gitlab-ssh/versions/1 | It contains the URI of a secret manager resource with an ssh key which has access to the target gitlab repository. | Only required for project factory and folder factory | +| _DEV_SECRET | E.g. projects/123456789/secrets/gitlab-ssh/versions/1 | It contains the URI of a secret manager resource with an ssh key which has access to the target gitlab repository. | This variable is only required to be configured for configuring the Skunkworks Development environment using the tf-cloudbuild-dev.yaml file. | +| _STAGE_SECRET | E.g. projects/123456789/secrets/gitlab-ssh/versions/1 | It contains the URI of a secret manager resource with an ssh key which has access to the target gitlab repository. | This variable is only required to be configured for configuring Skunkworks Staging environment using tf-cloudbuild-stage.yaml file. | +| _PROD_SECRET | E.g. projects/123456789/secrets/gitlab-ssh/versions/1 | It contains the URI of a secret manager resource with an ssh key which has access to the target gitlab repository. | This variable is only required to be configured for configuring the Skunkworks Production environment using tf-cloudbuild-prod.yaml file. | +| _FOLDER | E.g.
For skunkworks project update the value with: examples/guardrails/skunkworks.
For folder factory project update the value with: examples/guardrails/folder-factory.
For project factory project update the value with: examples/guardrails/project-factory + | It contains the directory with terraform configuration files which needs to be build when the trigger is invoked. | YES | +| _REPOSITORY_NAME | $(body.repository.name) | It contains the name of the repository. | YES | +| _STATE_BUCKET | E.g terraform-us-bucket | It contains the name of the GCS bucket where terraform state files are stored. | YES | +| _TO_SHA | $(body.after) | It contains the SHA of the commit that invoked the build. | YES | +| _SSH_REPOSITORY_NAME | E.g. git@gitlab.com:pawanphalak/cloud-build-terraform-iac.git | It contains the gitlab URL of the repository in SSH format. | YES | + + +### Service account for Cloud Build Jobs + +Cloud Build Jobs IAM permissions can be given by assigning a service account to a Cloud Build Trigger. The service account should have a Secret Manager Secret Accessor IAM role on the secret created during setting up the gitlab ssh keys. The service account should also have the required IAM roles to provision the GCP resources. + +### Troubleshooting + +1. [Cloud Build](https://cloud.google.com/build/docs/securing-builds/store-manage-build-logs#viewing_build_logs) logs can be viewed in the GCP console to troubleshoot any IAM permissions/CICD script errors. +Add below in cloudbuild.yaml to have a better understanding of tf plan + ```yaml + logsBucket: 'gs://${_LOG_BUCKET_NAME}' + options: + logging: GCS_ONLY + ``` +2. Validate the ssh keys have access to the gitlab clone repository. Open a terminal and run this command, replacing gitlab.example.com with your GitLab instance URL and id_gitlab with the path of the ssh keys with gitlab access: + ``` + ssh -T git@gitlab.example.com -i id_gitlab + ``` + +3. If the Cloud Build Job is not able to find any terraform configuration files, check if the path to the terraform configurations are set correctly in _FOLDER variable of cloud build trigger. If terraform configuration files are at the repository level keep the _FOLDER value as empty. More details about the dir parameter used with _FOLDER can be found [here](https://cloud.google.com/build/docs/build-config-file-schema#dir). + + +4. Do we need to create different cloud build triggers for different env’s? + + Yes, the cloud build trigger should be created separately for each environment. The skunkworks project is configured for different environments(development, staging and production). More details for updating cloudbuild inline configurations to configure with each environment are covered in the above section with skunkworks project setup. \ No newline at end of file diff --git a/examples/guardrails/cloudbuild/folder-factory/.github/workflows/terraform-deployment.yml b/examples/guardrails/cloudbuild/folder-factory/.github/workflows/terraform-deployment.yml deleted file mode 100644 index 70f88cb..0000000 --- a/examples/guardrails/cloudbuild/folder-factory/.github/workflows/terraform-deployment.yml +++ /dev/null @@ -1,91 +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 -# -# 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. - -name: 'Cloud Deployment' - -on: - push: - branches: - - main - -env: - STATE_BUCKET: ${{ secrets.STATE_BUCKET }} - WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} - SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} - -# OR SET MANUALLY -#env: -# STATE_BUCKET: 'XXXX' -# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' -# SERVICE_ACCOUNT: 'XXXX@XXXX' - -jobs: - - terraform: - name: 'terraform' - runs-on: ubuntu-latest - environment: production - - # Add "id-token" with the intended permissions. - permissions: - contents: 'read' - id-token: 'write' - - # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest - defaults: - run: - shell: bash - - steps: - # Checkout the repository to the GitHub Actions runner - - name: Checkout - uses: actions/checkout@v3 - - - id: 'auth' - name: 'Authenticate to Google Cloud' - uses: 'google-github-actions/auth@v0' - with: - token_format: 'access_token' - WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} - SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} - access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) - - # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token - - name: Setup Terraform - uses: hashicorp/setup-terraform@v1 - with: - cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} - - # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. - - name: Terraform Init - run: | - terraform init \ - -backend-config="bucket=$STATE_BUCKET" \ - -backend-config="prefix=$GITHUB_REPOSITORY" \ - - # Checks that all Terraform configuration files adhere to a canonical format - #- name: Terraform Format - # run: terraform fmt -check - - # Generates an execution plan for Terraform - - name: Terraform Plan - run: terraform plan - - # On push to main, build or change infrastructure according to Terraform configuration files - # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks - - name: Terraform Apply - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - run: terraform apply -auto-approve - - diff --git a/examples/guardrails/cloudbuild/folder-factory/.gitlab/workflows/cloudbuild.yaml b/examples/guardrails/cloudbuild/folder-factory/.gitlab/workflows/cloudbuild.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/examples/guardrails/cloudbuild/folder-factory/.jenkins/workflows/cloudbuild.yaml b/examples/guardrails/cloudbuild/folder-factory/.jenkins/workflows/cloudbuild.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/examples/guardrails/cloudbuild/folder-factory/.terraform-cloud/workflows/cloudbuild.yaml b/examples/guardrails/cloudbuild/folder-factory/.terraform-cloud/workflows/cloudbuild.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/examples/guardrails/cloudbuild/folder-factory/.terraform-enterprise/worflows/cloudbuild.yaml b/examples/guardrails/cloudbuild/folder-factory/.terraform-enterprise/worflows/cloudbuild.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/examples/guardrails/cloudbuild/project-factory/.github/workflows/terraform-deployment.yml b/examples/guardrails/cloudbuild/project-factory/.github/workflows/terraform-deployment.yml deleted file mode 100644 index 4099678..0000000 --- a/examples/guardrails/cloudbuild/project-factory/.github/workflows/terraform-deployment.yml +++ /dev/null @@ -1,94 +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 -# -# 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. - -name: 'Cloud Deployment' - -on: - push: - branches: - - main - -env: - STATE_BUCKET: ${{ secrets.STATE_BUCKET }} - FOLDER: ${{ secrets.FOLDER }} - WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} - SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} - -# OR SET MANUALLY -# -#env: -# STATE_BUCKET: 'XXXX' -# FOLDER: 'folders/XXXX' -# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' -# SERVICE_ACCOUNT: 'XXXX@XXXX' - -jobs: - - terraform: - name: 'terraform' - runs-on: ubuntu-latest - environment: production - - # Add "id-token" with the intended permissions. - permissions: - contents: 'read' - id-token: 'write' - - # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest - defaults: - run: - shell: bash - - steps: - # Checkout the repository to the GitHub Actions runner - - name: Checkout - uses: actions/checkout@v3 - - - id: 'auth' - name: 'Authenticate to Google Cloud' - uses: 'google-github-actions/auth@v0' - with: - token_format: 'access_token' - WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} - SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} - access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) - - # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token - - name: Setup Terraform - uses: hashicorp/setup-terraform@v1 - with: - cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} - - # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. - - name: Terraform Init - run: | - terraform init \ - -backend-config="bucket=$STATE_BUCKET" \ - -backend-config="prefix=$GITHUB_REPOSITORY" \ - - # Checks that all Terraform configuration files adhere to a canonical format - #- name: Terraform Format - # run: terraform fmt -check - - # Generates an execution plan for Terraform - - name: Terraform Plan - run: terraform plan -var "folder=$FOLDER" - - # On push to main, build or change infrastructure according to Terraform configuration files - # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks - - name: Terraform Apply - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - run: terraform apply -var "folder=$FOLDER" -auto-approve - - diff --git a/examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-dev.yml b/examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-dev.yml deleted file mode 100644 index 2fbf4c1..0000000 --- a/examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-dev.yml +++ /dev/null @@ -1,91 +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 -# -# 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. - -name: 'DEV Deployment' - -on: - push: - branches: - - dev - -env: - STATE_BUCKET: ${{ secrets.STATE_BUCKET }} - WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} - SERVICE_ACCOUNT: ${{ secrets.DEV_SERVICE_ACCOUNT }} - -# OR SET MANUALLY -#env: -# STATE_BUCKET: 'XXXX' -# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' -# SERVICE_ACCOUNT: 'XXXX@XXXX' - -jobs: - - terraform: - name: 'terraform' - runs-on: ubuntu-latest - environment: production - - # Add "id-token" with the intended permissions. - permissions: - contents: 'read' - id-token: 'write' - - # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest - defaults: - run: - shell: bash - - steps: - # Checkout the repository to the GitHub Actions runner - - name: Checkout - uses: actions/checkout@v3 - - - id: 'auth' - name: 'Authenticate to Google Cloud' - uses: 'google-github-actions/auth@v0' - with: - token_format: 'access_token' - WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} - SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} - access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) - - # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token - - name: Setup Terraform - uses: hashicorp/setup-terraform@v1 - with: - cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} - - # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. - - name: Terraform Init - run: | - terraform init \ - -backend-config="bucket=$STATE_BUCKET" \ - -backend-config="prefix=$GITHUB_REPOSITORY" \ - - # Checks that all Terraform configuration files adhere to a canonical format - #- name: Terraform Format - # run: terraform fmt -check - - # Generates an execution plan for Terraform - - name: Terraform Plan - run: terraform plan - - # On push to main, build or change infrastructure according to Terraform configuration files - # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks - - name: Terraform Apply - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - run: terraform apply -auto-approve - - diff --git a/examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-prod.yml b/examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-prod.yml deleted file mode 100644 index 4159feb..0000000 --- a/examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-prod.yml +++ /dev/null @@ -1,91 +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 -# -# 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. - -name: 'PROD Deployment' - -on: - push: - branches: - - main - -env: - STATE_BUCKET: ${{ secrets.STATE_BUCKET }} - WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} - SERVICE_ACCOUNT: ${{ secrets.PROD_SERVICE_ACCOUNT }} - -# OR SET MANUALLY -#env: -# STATE_BUCKET: 'XXXX' -# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' -# SERVICE_ACCOUNT: 'XXXX@XXXX' - -jobs: - - terraform: - name: 'terraform' - runs-on: ubuntu-latest - environment: production - - # Add "id-token" with the intended permissions. - permissions: - contents: 'read' - id-token: 'write' - - # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest - defaults: - run: - shell: bash - - steps: - # Checkout the repository to the GitHub Actions runner - - name: Checkout - uses: actions/checkout@v3 - - - id: 'auth' - name: 'Authenticate to Google Cloud' - uses: 'google-github-actions/auth@v0' - with: - token_format: 'access_token' - WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} - SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} - access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) - - # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token - - name: Setup Terraform - uses: hashicorp/setup-terraform@v1 - with: - cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} - - # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. - - name: Terraform Init - run: | - terraform init \ - -backend-config="bucket=$STATE_BUCKET" \ - -backend-config="prefix=$GITHUB_REPOSITORY" \ - - # Checks that all Terraform configuration files adhere to a canonical format - #- name: Terraform Format - # run: terraform fmt -check - - # Generates an execution plan for Terraform - - name: Terraform Plan - run: terraform plan - - # On push to main, build or change infrastructure according to Terraform configuration files - # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks - - name: Terraform Apply - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - run: terraform apply -auto-approve - - diff --git a/examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-stage.yml b/examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-stage.yml deleted file mode 100644 index 0f20fa0..0000000 --- a/examples/guardrails/cloudbuild/skunkworks/.github/workflows/tf-actions-stage.yml +++ /dev/null @@ -1,91 +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 -# -# 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. - -name: 'STAGE Deployment' - -on: - push: - branches: - - stage - -env: - STATE_BUCKET: ${{ secrets.STATE_BUCKET }} - WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} - SERVICE_ACCOUNT: ${{ secrets.STAGE_SERVICE_ACCOUNT }} - -# OR SET MANUALLY -#env: -# STATE_BUCKET: 'XXXX' -# WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' -# SERVICE_ACCOUNT: 'XXXX@XXXX' - -jobs: - - terraform: - name: 'terraform' - runs-on: ubuntu-latest - environment: production - - # Add "id-token" with the intended permissions. - permissions: - contents: 'read' - id-token: 'write' - - # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest - defaults: - run: - shell: bash - - steps: - # Checkout the repository to the GitHub Actions runner - - name: Checkout - uses: actions/checkout@v3 - - - id: 'auth' - name: 'Authenticate to Google Cloud' - uses: 'google-github-actions/auth@v0' - with: - token_format: 'access_token' - WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} - SERVICE_ACCOUNT: ${{ env.SERVICE_ACCOUNT }} - access_token_lifetime: '300s' # optional, default: '3600s' (1 hour) - - # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token - - name: Setup Terraform - uses: hashicorp/setup-terraform@v1 - with: - cli_config_credentials_token: ${{ steps.auth.outputs.access_token }} - - # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. - - name: Terraform Init - run: | - terraform init \ - -backend-config="bucket=$STATE_BUCKET" \ - -backend-config="prefix=$GITHUB_REPOSITORY" \ - - # Checks that all Terraform configuration files adhere to a canonical format - #- name: Terraform Format - # run: terraform fmt -check - - # Generates an execution plan for Terraform - - name: Terraform Plan - run: terraform plan - - # On push to main, build or change infrastructure according to Terraform configuration files - # Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks - - name: Terraform Apply - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - run: terraform apply -auto-approve - - From 7d5d46a8c60088753e166350513e23a4358d2d83 Mon Sep 17 00:00:00 2001 From: pawanphalak <105412459+pawanphalak@users.noreply.github.com> Date: Mon, 13 Mar 2023 10:15:36 +0530 Subject: [PATCH 110/132] Readme updates for cloudbuild. Created main readme file at cloudbuild directory --- examples/guardrails/cloudbuild/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index e7e6fe5..5f10247 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -32,7 +32,8 @@ Gitlab repository can be build with Cloud build by webhook triggers. Following a * The cloud build webhook created with sample cloud build inline configuration can be triggered to validate the working of the webhook trigger. * Navigate to Cloudbuild > Settings > Webhooks page to send the webhook trigger event by clicking the push event(or the event for which the gitlab trigger is configured): -IMAGE +![webhook](https://user-images.githubusercontent.com/105412459/224610214-4d1b1988-e7ce-4e6a-9b01-09db3059cb8e.png) + * This will invoke the cloud build job in the GCP console. @@ -86,4 +87,4 @@ Add below in cloudbuild.yaml to have a better understanding of tf plan 4. Do we need to create different cloud build triggers for different env’s? - Yes, the cloud build trigger should be created separately for each environment. The skunkworks project is configured for different environments(development, staging and production). More details for updating cloudbuild inline configurations to configure with each environment are covered in the above section with skunkworks project setup. \ No newline at end of file + Yes, the cloud build trigger should be created separately for each environment. The skunkworks project is configured for different environments(development, staging and production). More details for updating cloudbuild inline configurations to configure with each environment are covered in the above section with skunkworks project setup. From 3b70d6c9321ff1fd42fe07f072f7be2f9a4ba757 Mon Sep 17 00:00:00 2001 From: pawanphalak Date: Mon, 13 Mar 2023 10:17:25 +0530 Subject: [PATCH 111/132] Readme updates for cloudbuild. Created main readme file at cloudbuild directory --- examples/guardrails/cloudbuild/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/guardrails/cloudbuild/README.md b/examples/guardrails/cloudbuild/README.md index 5f10247..a8ac851 100644 --- a/examples/guardrails/cloudbuild/README.md +++ b/examples/guardrails/cloudbuild/README.md @@ -56,8 +56,7 @@ Gitlab repository can be build with Cloud build by webhook triggers. Following a | _DEV_SECRET | E.g. projects/123456789/secrets/gitlab-ssh/versions/1 | It contains the URI of a secret manager resource with an ssh key which has access to the target gitlab repository. | This variable is only required to be configured for configuring the Skunkworks Development environment using the tf-cloudbuild-dev.yaml file. | | _STAGE_SECRET | E.g. projects/123456789/secrets/gitlab-ssh/versions/1 | It contains the URI of a secret manager resource with an ssh key which has access to the target gitlab repository. | This variable is only required to be configured for configuring Skunkworks Staging environment using tf-cloudbuild-stage.yaml file. | | _PROD_SECRET | E.g. projects/123456789/secrets/gitlab-ssh/versions/1 | It contains the URI of a secret manager resource with an ssh key which has access to the target gitlab repository. | This variable is only required to be configured for configuring the Skunkworks Production environment using tf-cloudbuild-prod.yaml file. | -| _FOLDER | E.g.
For skunkworks project update the value with: examples/guardrails/skunkworks.
For folder factory project update the value with: examples/guardrails/folder-factory.
For project factory project update the value with: examples/guardrails/project-factory - | It contains the directory with terraform configuration files which needs to be build when the trigger is invoked. | YES | +| _FOLDER | E.g.
For skunkworks project update the value with: examples/guardrails/skunkworks.
For folder factory project update the value with: examples/guardrails/folder-factory.
For project factory project update the value with: examples/guardrails/project-factory | It contains the directory with terraform configuration files which needs to be build when the trigger is invoked. | YES | | _REPOSITORY_NAME | $(body.repository.name) | It contains the name of the repository. | YES | | _STATE_BUCKET | E.g terraform-us-bucket | It contains the name of the GCS bucket where terraform state files are stored. | YES | | _TO_SHA | $(body.after) | It contains the SHA of the commit that invoked the build. | YES | From b61e9996c92424a4c1a2a54370aaa0d78b7bb3f8 Mon Sep 17 00:00:00 2001 From: agutta Date: Mon, 13 Mar 2023 13:50:54 -0500 Subject: [PATCH 112/132] Update README.md --- .../cloudbuild/folder-factory/README.md | 36 ++----------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/examples/guardrails/cloudbuild/folder-factory/README.md b/examples/guardrails/cloudbuild/folder-factory/README.md index aa5036b..a5741fe 100644 --- a/examples/guardrails/cloudbuild/folder-factory/README.md +++ b/examples/guardrails/cloudbuild/folder-factory/README.md @@ -1,39 +1,7 @@ # Folder Factory -This is a template for a DevOps folder factory. - -It can be used with [https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory](https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory) and is intended to house the folder configurations: - -![Screenshot 2022-05-10 12 00 19 PM](https://user-images.githubusercontent.com/94000358/169809437-aaa8538e-3ffc-48b3-9028-84e4995de150.png) - -Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. - -The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. - -## Repository Configuration -This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. - -If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw - -After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: - -Secret configuration - -or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: -``` -env: - STATE_BUCKET: 'XXXX' - # The GCS bucket to store the terraform state - WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' - # The workload identity provider that should be used for this repository. - SERVICE_ACCOUNT: 'XXXX@XXXX' - # The service account that should be used for this repository. -``` - -## Setting up folders - The folder factory will: -- create a folders with defined organisational policies +- create folder(s) with defined organization policies It uses YAML configuration files for every folder with the following sample structure: ``` @@ -57,3 +25,5 @@ iam: ``` Every folder is defined with its own yaml file located in the following [Folder](data/folders). + +> Detailed instructions of CloudBuild trigger setup can be found at [README](./../../../README.md). From f670985e53e3b5f71397dc216906e48b6a2e77c9 Mon Sep 17 00:00:00 2001 From: agutta Date: Mon, 13 Mar 2023 13:51:45 -0500 Subject: [PATCH 113/132] Update README.md --- .../cloudbuild/project-factory/README.md | 38 +------------------ 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/examples/guardrails/cloudbuild/project-factory/README.md b/examples/guardrails/cloudbuild/project-factory/README.md index ce96a31..c0abed0 100644 --- a/examples/guardrails/cloudbuild/project-factory/README.md +++ b/examples/guardrails/cloudbuild/project-factory/README.md @@ -1,41 +1,5 @@ # Project Factory -This is a template for a DevOps project factory. - -It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory) and is intended to house the projects of a specified folder: - -Overview - -Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. - -Screenshot 2023-03-10 at 03 09 10 - -The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. - -## Repository Configuration -This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. - -If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw - -After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: - -Secret settings - -or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: -``` -env: - STATE_BUCKET: 'XXXX' - # The GCS bucket to store the terraform state - FOLDER: 'folders/XXXX' - # The folder under which the projects should be created - WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' - # The workload identity provider that should be used for this repository. - SERVICE_ACCOUNT: 'XXXX@XXXX' - # The service account that should be used for this repository. -``` - -## Setting up projects - The project factory will: - create a service account with defined rights - create a project within the folder @@ -73,3 +37,5 @@ repo_branch: dev ``` Every project is defined with its own file located in the [Project Folder](data/projects). + +> Detailed instructions of CloudBuild trigger setup can be found at [README](./../../../README.md). From 87cb9688b51921afcf020e19bf2fbe1e65408cd2 Mon Sep 17 00:00:00 2001 From: agutta Date: Mon, 13 Mar 2023 13:52:10 -0500 Subject: [PATCH 114/132] Update README.md --- examples/guardrails/cloudbuild/project-factory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/cloudbuild/project-factory/README.md b/examples/guardrails/cloudbuild/project-factory/README.md index c0abed0..a566595 100644 --- a/examples/guardrails/cloudbuild/project-factory/README.md +++ b/examples/guardrails/cloudbuild/project-factory/README.md @@ -38,4 +38,4 @@ repo_branch: dev Every project is defined with its own file located in the [Project Folder](data/projects). -> Detailed instructions of CloudBuild trigger setup can be found at [README](./../../../README.md). +> Detailed instructions of CloudBuild trigger setup can be found at [README](./README.md). From 1b1d95cb716a4f001b8160e0a973b44301135f63 Mon Sep 17 00:00:00 2001 From: agutta Date: Mon, 13 Mar 2023 13:52:24 -0500 Subject: [PATCH 115/132] Update README.md --- examples/guardrails/cloudbuild/project-factory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/cloudbuild/project-factory/README.md b/examples/guardrails/cloudbuild/project-factory/README.md index a566595..c6a1857 100644 --- a/examples/guardrails/cloudbuild/project-factory/README.md +++ b/examples/guardrails/cloudbuild/project-factory/README.md @@ -38,4 +38,4 @@ repo_branch: dev Every project is defined with its own file located in the [Project Folder](data/projects). -> Detailed instructions of CloudBuild trigger setup can be found at [README](./README.md). +> Detailed instructions of CloudBuild trigger setup can be found at [README](./../README.md). From 95b50b3ddb7ed30dab1a9c675eec5b5e8de38d8f Mon Sep 17 00:00:00 2001 From: agutta Date: Mon, 13 Mar 2023 13:52:49 -0500 Subject: [PATCH 116/132] Update README.md --- examples/guardrails/cloudbuild/folder-factory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/cloudbuild/folder-factory/README.md b/examples/guardrails/cloudbuild/folder-factory/README.md index a5741fe..4cb7699 100644 --- a/examples/guardrails/cloudbuild/folder-factory/README.md +++ b/examples/guardrails/cloudbuild/folder-factory/README.md @@ -26,4 +26,4 @@ iam: Every folder is defined with its own yaml file located in the following [Folder](data/folders). -> Detailed instructions of CloudBuild trigger setup can be found at [README](./../../../README.md). +> Detailed instructions of CloudBuild trigger setup can be found at [README](./../README.md). From b2e50e5f4c92100fcffb5691341cfcbe5ad69946 Mon Sep 17 00:00:00 2001 From: agutta Date: Mon, 13 Mar 2023 13:53:55 -0500 Subject: [PATCH 117/132] Update README.md --- .../cloudbuild/skunkworks/README.md | 30 ++----------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/examples/guardrails/cloudbuild/skunkworks/README.md b/examples/guardrails/cloudbuild/skunkworks/README.md index 4e50db5..a6bb8ce 100644 --- a/examples/guardrails/cloudbuild/skunkworks/README.md +++ b/examples/guardrails/cloudbuild/skunkworks/README.md @@ -1,31 +1,5 @@ # Skunkworks - IaC Kickstarter Template -This is a template for an IaC kickstarter repository. +The Skunkworks - IaC Kickstarter is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. -Screenshot 2023-03-10 at 03 09 49 - -The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. It is based on the following "ideal" pipeline: - -![CloudBuild Pipeline](https://user-images.githubusercontent.com/94000358/224207506-8f61de84-6039-48eb-be36-59e68f6f5ef2.png) - -This template creates a bucket in the specified target environment. - -## Repository Configuration -This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. - -If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw - -After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: - -Secret configuration - -or by modifing the [Workflow Action Files](.github/workflows/) and setting the environment variables: -``` -env: - STATE_BUCKET: 'XXXX' - # The GCS bucket to store the terraform state - WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' - # The workload identity provider that should be used for this repository. - SERVICE_ACCOUNT: 'XXXX@XXXX' - # The service account that should be used for this repository. -``` +> Detailed instructions of CloudBuild trigger setup can be found at [README](./../README.md). From 93d17840e1682c89d5298620ff453aae8482b5c2 Mon Sep 17 00:00:00 2001 From: Shubham Singh Date: Fri, 17 Mar 2023 21:30:47 +0530 Subject: [PATCH 118/132] config-validator changes --- .../.bitbucket/bitbucket-pipelines.yml | 33 +++++++ .../.bitbucket/bitbucket-pipelines.yml | 33 +++++++ .../.bitbucket/bitbucket-pipelines.yml | 99 +++++++++++++++++++ .../guardrails/bitbucket/skunkworks/README.md | 2 +- 4 files changed, 166 insertions(+), 1 deletion(-) diff --git a/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml index 21bbc34..2b9262c 100644 --- a/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml +++ b/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml @@ -43,6 +43,39 @@ pipelines: - ls /usr/bin artifacts: - terraform + - step: + name: tf-policy validate + oidc: true + script: + - if [[ "$TERRAFORM_POLICY_VALIDATE" == "true" ]]; then + - cp terraform /usr/bin/ + - export TF_PLAN_NAME=plan.out + - export TF_ROOT=tf_root + - mkdir -p $TF_ROOT + - ls . /usr/bin/ + - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - ./get_oidctoken.sh + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json + - apt-get install google-cloud-sdk-terraform-tools -y + - git clone $POLICY_LIBRARY_REPO $TF_ROOT/policy-repo + - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - /usr/bin/terraform plan -out $TF_PLAN_NAME + - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json + - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) + - ret_val=$? + - if [ $ret_val -eq 2 ]; + - then + - echo "$violations" + - echo "Violations found, not proceeding with terraform apply" + - exit 1 + - elif [ $ret_val -ne 0 ]; + - then + - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" + - exit 1 + - else + - echo "No policy violations detected; proceeding with terraform apply" + - fi + - fi - step: name: master branch execution tf-apply oidc: true diff --git a/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml index cada72f..2170db0 100644 --- a/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml +++ b/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml @@ -43,6 +43,39 @@ pipelines: - ls /usr/bin artifacts: - terraform + - step: + name: tf-policy validate + oidc: true + script: + - if [[ "$TERRAFORM_POLICY_VALIDATE" == "true" ]]; then + - cp terraform /usr/bin/ + - export TF_PLAN_NAME=plan.out + - export TF_ROOT=tf_root + - mkdir -p $TF_ROOT + - ls . /usr/bin/ + - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - ./get_oidctoken.sh + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json + - apt-get install google-cloud-sdk-terraform-tools -y + - git clone $POLICY_LIBRARY_REPO $TF_ROOT/policy-repo + - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - /usr/bin/terraform plan -out $TF_PLAN_NAME + - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json + - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) + - ret_val=$? + - if [ $ret_val -eq 2 ]; + - then + - echo "$violations" + - echo "Violations found, not proceeding with terraform apply" + - exit 1 + - elif [ $ret_val -ne 0 ]; + - then + - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" + - exit 1 + - else + - echo "No policy violations detected; proceeding with terraform apply" + - fi + - fi - step: name: master branch execution tf-apply oidc: true diff --git a/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml index ef8f334..833d824 100644 --- a/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml +++ b/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml @@ -34,6 +34,39 @@ pipelines: - ls /usr/bin artifacts: - terraform + - step: + name: tf-policy validate + oidc: true + script: + - if [[ "$TERRAFORM_POLICY_VALIDATE" == "true" ]]; then + - cp terraform /usr/bin/ + - export TF_PLAN_NAME=plan.out + - export TF_ROOT=tf_root + - mkdir -p $TF_ROOT + - ls . /usr/bin/ + - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - ./get_oidctoken.sh + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json + - apt-get install google-cloud-sdk-terraform-tools -y + - git clone $POLICY_LIBRARY_REPO $TF_ROOT/policy-repo + - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - /usr/bin/terraform plan -out $TF_PLAN_NAME + - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json + - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) + - ret_val=$? + - if [ $ret_val -eq 2 ]; + - then + - echo "$violations" + - echo "Violations found, not proceeding with terraform apply" + - exit 1 + - elif [ $ret_val -ne 0 ]; + - then + - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" + - exit 1 + - else + - echo "No policy violations detected; proceeding with terraform apply" + - fi + - fi - step: name: master branch execution tf-apply oidc: true @@ -58,6 +91,39 @@ pipelines: - ls /usr/bin artifacts: - terraform + - step: + name: tf-policy validate + oidc: true + script: + - if [[ "$TERRAFORM_POLICY_VALIDATE" == "true" ]]; then + - cp terraform /usr/bin/ + - export TF_PLAN_NAME=plan.out + - export TF_ROOT=tf_root + - mkdir -p $TF_ROOT + - ls . /usr/bin/ + - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - ./get_oidctoken.sh + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json + - apt-get install google-cloud-sdk-terraform-tools -y + - git clone $POLICY_LIBRARY_REPO $TF_ROOT/policy-repo + - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - /usr/bin/terraform plan -out $TF_PLAN_NAME + - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json + - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) + - ret_val=$? + - if [ $ret_val -eq 2 ]; + - then + - echo "$violations" + - echo "Violations found, not proceeding with terraform apply" + - exit 1 + - elif [ $ret_val -ne 0 ]; + - then + - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" + - exit 1 + - else + - echo "No policy violations detected; proceeding with terraform apply" + - fi + - fi - step: name: dev branch execution tf-apply oidc: true @@ -82,6 +148,39 @@ pipelines: - ls /usr/bin artifacts: - terraform + - step: + name: tf-policy validate + oidc: true + script: + - if [[ "$TERRAFORM_POLICY_VALIDATE" == "true" ]]; then + - cp terraform /usr/bin/ + - export TF_PLAN_NAME=plan.out + - export TF_ROOT=tf_root + - mkdir -p $TF_ROOT + - ls . /usr/bin/ + - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - ./get_oidctoken.sh + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json + - apt-get install google-cloud-sdk-terraform-tools -y + - git clone $POLICY_LIBRARY_REPO $TF_ROOT/policy-repo + - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - /usr/bin/terraform plan -out $TF_PLAN_NAME + - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json + - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) + - ret_val=$? + - if [ $ret_val -eq 2 ]; + - then + - echo "$violations" + - echo "Violations found, not proceeding with terraform apply" + - exit 1 + - elif [ $ret_val -ne 0 ]; + - then + - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" + - exit 1 + - else + - echo "No policy violations detected; proceeding with terraform apply" + - fi + - fi - step: name: prod branch execution tf-apply oidc: true diff --git a/examples/guardrails/bitbucket/skunkworks/README.md b/examples/guardrails/bitbucket/skunkworks/README.md index 4a2c84b..5a64235 100644 --- a/examples/guardrails/bitbucket/skunkworks/README.md +++ b/examples/guardrails/bitbucket/skunkworks/README.md @@ -26,4 +26,4 @@ env: # The workload identity provider that should be used for this repository. SERVICE_ACCOUNT: 'XXXX@XXXX' # The service account that should be used for this repository. -``` +``` \ No newline at end of file From d0904bc9060f5fbd4e7e4ae43be2df2f0dc33963 Mon Sep 17 00:00:00 2001 From: Shubham Singh Date: Mon, 20 Mar 2023 01:55:51 +0530 Subject: [PATCH 119/132] updated the readme to follow .gitlab readme --- .../bitbucket/folder-factory/README.md | 51 ++++++++++++++++++ .../bitbucket/project-factory/README.md | 52 ++++++++++++++++++ .../guardrails/bitbucket/skunkworks/README.md | 54 ++++++++++++++++++- 3 files changed, 156 insertions(+), 1 deletion(-) diff --git a/examples/guardrails/bitbucket/folder-factory/README.md b/examples/guardrails/bitbucket/folder-factory/README.md index c7d3df8..d842184 100644 --- a/examples/guardrails/bitbucket/folder-factory/README.md +++ b/examples/guardrails/bitbucket/folder-factory/README.md @@ -57,3 +57,54 @@ iam: ``` Every folder is defined with its own yaml file located in the following [Folder](data/folders). + + +## How to run this stage +### Prerequisites + +Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. There is a sample code provided in “folder.yaml.sample” to create a folder and for terraform to create a folder minimum below permissions are required. +“Folder Creator” or “Folder Admin” at org level +“Organization Policy Admin” at org level + + +### Installation Steps +From the folder-factory Gitlab project page +* CICD configuration file path + Navigate to Pipelines > Run Pipeline > select branch > select pipeline + Update “CI/CD configuration file” to point to the root of repository, `bitbucket-pipelines.yml` + +* CI/CD variables + Navigate to Deployments > Configure > Add Variables + Add the variables to the pipeline as described in the table below. + +### Terraform config validator +The pipeline has an option to utilise the integrated config validator (gcloud terraform vet) to impose constraints on your terraform configuration. You can enable it by setting the CI/CD Variable $TERRAFORM_POLICY_VALIDATE to "true" and providing the policy-library repo URL to $POLICY_LIBRARY_REPO variable. See the below for details on the Variables to be set on the CI/CD pipeline. + + +| Variable | | Example Value | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | +| TERRAFORM_VERSION | Version of Terraform to execute. Terraform will be installed at the beginning of every job. | 1.3.7 | +| PROJECT_NAME | The project containing the service account that has permission to communicate with the WIF provider. Should be created as part of Project Factory. | bitbucket-connect-prj | +| GCP_PROJECT_NUMBER | Project number for the project that hosts the WIF provider | 107999111999 | +| SERVICE_ACCOUNT_NAME | The name of the service account that will be used to deploy. Must be hosted in PROJECT_NAME. | bb-service-acct | +| BUCKET_PATH | A state bucket that will hold the terraform state. This bucket must previously exist and the service account must have permission to read/write to it. | gcs-state-bucket-name | +| POLICY_LIBRARY_REPO | https://github.com/GoogleCloudPlatform/policy-library | The public repo where the policies are hosted | +| TERRAFORM_POLICY_VALIDATE | This is a flag that should be set to “true” in order to enable the policy validation | Should be set to “true” to enable validation | +| workload_identity_pool_id | | bitbucket-test-pool | +| workload_identity_provider_id | | bitbucket-test-provider | | + +* Once the prerequisites are set up, any commit to the remote main branch with changes to *.tf, *.tfvars, data/*, modules/* files should trigger the pipeline. + + +* get_oidctoken.sh should run successfully in the pipeline if the workload identity federation is configured as required. + +### Pipeline Workflow Overview +The complete workflow comprises of 4-5 stages and 2 before-script jobs + * setup terraform : + * This step should download the terraform from internet and keep that as an artifacts to be used in later stages + * Stages: + * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages + * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts, this depends on the branch + * policy-validate: Runs gcloud terraform vet against the terraform code with the constraints in the specified repository. + * apply: This is executed for specified list of branches, currently main/master + diff --git a/examples/guardrails/bitbucket/project-factory/README.md b/examples/guardrails/bitbucket/project-factory/README.md index 34a9ab6..f40ee7b 100644 --- a/examples/guardrails/bitbucket/project-factory/README.md +++ b/examples/guardrails/bitbucket/project-factory/README.md @@ -73,3 +73,55 @@ repo_branch: dev ``` Every project is defined with its own file located in the [Project Folder](data/projects). + + + +## How to run this stage +### Prerequisites + +Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. There is a sample code provided in “folder.yaml.sample” to create a folder and for terraform to create a folder minimum below permissions are required. +“Folder Creator” or “Folder Admin” at org level +“Organization Policy Admin” at org level + + +### Installation Steps +From the folder-factory Gitlab project page +* CICD configuration file path + Navigate to Pipelines > Run Pipeline > select branch > select pipeline + Update “CI/CD configuration file” to point to the root of repository, `bitbucket-pipelines.yml` + +* CI/CD variables + Navigate to Deployments > Configure > Add Variables + Add the variables to the pipeline as described in the table below. + +### Terraform config validator +The pipeline has an option to utilise the integrated config validator (gcloud terraform vet) to impose constraints on your terraform configuration. You can enable it by setting the CI/CD Variable $TERRAFORM_POLICY_VALIDATE to "true" and providing the policy-library repo URL to $POLICY_LIBRARY_REPO variable. See the below for details on the Variables to be set on the CI/CD pipeline. + + +| Variable | | Example Value | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | +| TERRAFORM_VERSION | Version of Terraform to execute. Terraform will be installed at the beginning of every job. | 1.3.7 | +| PROJECT_NAME | The project containing the service account that has permission to communicate with the WIF provider. Should be created as part of Project Factory. | bitbucket-connect-prj | +| GCP_PROJECT_NUMBER | Project number for the project that hosts the WIF provider | 107999111999 | +| SERVICE_ACCOUNT_NAME | The name of the service account that will be used to deploy. Must be hosted in PROJECT_NAME. | bb-service-acct | +| BUCKET_PATH | A state bucket that will hold the terraform state. This bucket must previously exist and the service account must have permission to read/write to it. | gcs-state-bucket-name | +| POLICY_LIBRARY_REPO | https://github.com/GoogleCloudPlatform/policy-library | The public repo where the policies are hosted | +| TERRAFORM_POLICY_VALIDATE | This is a flag that should be set to “true” in order to enable the policy validation | Should be set to “true” to enable validation | +| workload_identity_pool_id | | bitbucket-test-pool | +| workload_identity_provider_id | | bitbucket-test-provider | | + +* Once the prerequisites are set up, any commit to the remote main branch with changes to *.tf, *.tfvars, data/*, modules/* files should trigger the pipeline. + + +* get_oidctoken.sh should run successfully in the pipeline if the workload identity federation is configured as required. + +### Pipeline Workflow Overview +The complete workflow comprises of 4-5 stages and 2 before-script jobs + * setup terraform : + * This step should download the terraform from internet and keep that as an artifacts to be used in later stages + * Stages: + * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages + * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts, this depends on the branch + * policy-validate: Runs gcloud terraform vet against the terraform code with the constraints in the specified repository. + * apply: This is executed for specified list of branches, currently main/master + diff --git a/examples/guardrails/bitbucket/skunkworks/README.md b/examples/guardrails/bitbucket/skunkworks/README.md index 5a64235..c04ff25 100644 --- a/examples/guardrails/bitbucket/skunkworks/README.md +++ b/examples/guardrails/bitbucket/skunkworks/README.md @@ -26,4 +26,56 @@ env: # The workload identity provider that should be used for this repository. SERVICE_ACCOUNT: 'XXXX@XXXX' # The service account that should be used for this repository. -``` \ No newline at end of file +``` + + + +## How to run this stage +### Prerequisites + +Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. There is a sample code provided in “folder.yaml.sample” to create a folder and for terraform to create a folder minimum below permissions are required. +“Folder Creator” or “Folder Admin” at org level +“Organization Policy Admin” at org level + + +### Installation Steps +From the folder-factory Gitlab project page +* CICD configuration file path + Navigate to Pipelines > Run Pipeline > select branch > select pipeline + Update “CI/CD configuration file” to point to the root of repository, `bitbucket-pipelines.yml` + +* CI/CD variables + Navigate to Deployments > Configure > Add Variables + Add the variables to the pipeline as described in the table below. + +### Terraform config validator +The pipeline has an option to utilise the integrated config validator (gcloud terraform vet) to impose constraints on your terraform configuration. You can enable it by setting the CI/CD Variable $TERRAFORM_POLICY_VALIDATE to "true" and providing the policy-library repo URL to $POLICY_LIBRARY_REPO variable. See the below for details on the Variables to be set on the CI/CD pipeline. + + +| Variable | | Example Value | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | +| TERRAFORM_VERSION | Version of Terraform to execute. Terraform will be installed at the beginning of every job. | 1.3.7 | +| PROJECT_NAME | The project containing the service account that has permission to communicate with the WIF provider. Should be created as part of Project Factory. | bitbucket-connect-prj | +| GCP_PROJECT_NUMBER | Project number for the project that hosts the WIF provider | 107999111999 | +| SERVICE_ACCOUNT_NAME | The name of the service account that will be used to deploy. Must be hosted in PROJECT_NAME. | bb-service-acct | +| BUCKET_PATH | A state bucket that will hold the terraform state. This bucket must previously exist and the service account must have permission to read/write to it. | gcs-state-bucket-name | +| POLICY_LIBRARY_REPO | https://github.com/GoogleCloudPlatform/policy-library | The public repo where the policies are hosted | +| TERRAFORM_POLICY_VALIDATE | This is a flag that should be set to “true” in order to enable the policy validation | Should be set to “true” to enable validation | +| workload_identity_pool_id | | bitbucket-test-pool | +| workload_identity_provider_id | | bitbucket-test-provider | | + +* Once the prerequisites are set up, any commit to the remote main branch with changes to *.tf, *.tfvars, data/*, modules/* files should trigger the pipeline. + + +* get_oidctoken.sh should run successfully in the pipeline if the workload identity federation is configured as required. + +### Pipeline Workflow Overview +The complete workflow comprises of 4-5 stages and 2 before-script jobs + * setup terraform : + * This step should download the terraform from internet and keep that as an artifacts to be used in later stages + * Stages: + * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages + * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts, this depends on the branch + * policy-validate: Runs gcloud terraform vet against the terraform code with the constraints in the specified repository. + * apply: This is executed for specified list of branches, currently defined branches are prod/dev/staging + From e31b8cfa7ab34dc3a04f59421e46125313f93aae Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Mon, 20 Mar 2023 19:32:39 +0530 Subject: [PATCH 120/132] azure devops config validator step added --- examples/guardrails/azuredevops/README.md | 21 ++++ .../folder-factory/azure-pipeline.yml | 92 ++++++++++++---- .../project-factory/azure-pipeline.yml | 103 +++++++++++++----- ...ith VPC Service Controls, you can defin.md | 3 + .../azuredevops/skunkworks/azure-pipeline.yml | 103 +++++++++++++----- 5 files changed, 247 insertions(+), 75 deletions(-) create mode 100644 examples/guardrails/azuredevops/README.md create mode 100644 examples/guardrails/azuredevops/skunkworks/With VPC Service Controls, you can defin.md diff --git a/examples/guardrails/azuredevops/README.md b/examples/guardrails/azuredevops/README.md new file mode 100644 index 0000000..c09f4bc --- /dev/null +++ b/examples/guardrails/azuredevops/README.md @@ -0,0 +1,21 @@ +# Getting started + +This workflow covers the steps to setup ADO CICD pipeline for terraform. The setup involves setting up ADO repository and the corresponding CI/CD settings and variables. The pipeline triggers based on select events (like push to specific branches), authenticates to the specified service account using Workload Identity federation and runs the pipeline to deploy infrastructure using terraform in GCP.\ +A more comprehensive description of DevOps & GitOps principles can be found at [DevOps README](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/README.md). + +## Implementation Process + +The setup consists of configuring folder-factory, project-factory and skunkworks as three ADO/gitlab repositories. The pre-requisites and the setup are detailed below for each of the repositories. + +Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account.\ +To use Azure Devops with GCP Deployments, we can leverage on [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) between Azure Devops and Google Cloud. This will be possible by configuring the Workload Identity Federation to trust OIDC tokens generated for a specific workflow in Azure Devops. + +## ADO Prerequisites + +- Create a Project in ADO +- Add a repo called "folder factory" and copy code from [devops folder factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/folder-factory) into it +- Add repo called "project factory" and copy code from [devops project factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/project-factory) into it +- Add a repo called "skunkworks" and copy code from [devops skunkworks repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/skunkworks) into it. + +> .\ +Once ADO set is completed go to [Folder Factory](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/examples/guardrails/gitlab/folder-factory) diff --git a/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml b/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml index 22538a0..108ef15 100644 --- a/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml +++ b/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml @@ -3,16 +3,17 @@ name: "$(Date:yyyyMMdd)$(Rev:.r)" trigger: branches: include: - - main + - none variables: - azureSubscription: "" - projectID: "" - workloadIdentityPoolProvider: "" - Projectnumber: "" - serviceaccount: "" - workloadIdentityPools: "" + azureSubscription: "azuretogcp" + projectID: "azuretogcp-374609" + workloadIdentityPoolProvider: "azure" + Projectnumber: "878833087983" + serviceaccount: "azure-sa" + workloadIdentityPools: "azure" + policyValidate: "true" pool: @@ -23,7 +24,7 @@ stages: - stage: auth displayName: "GCP WIF Auth" jobs: - - job: governance_pipeline + - job: governance_pipeline_azure timeoutInMinutes: 30 steps: - task: charleszipp.azure-pipelines-tasks-terraform.azure-pipelines-tasks-terraform-installer.TerraformInstaller@0 @@ -67,16 +68,10 @@ stages: echo "##vso[task.setvariable variable=ACCESS_TOKEN]$ACCESS_TOKEN" echo $ACCESS_TOKEN - - - task: downloadgitlabrepository@1 - displayName: "download repo" - inputs: - connection: 'Gauravgitlab' - definition: '43337570' - versionSelector: 'latestDefaultBranch' + - task: AzureCLI@2 - displayName: "Folder factory" + displayName: "Terraform plan" inputs: azureSubscription: "$(azureSubscription)" scriptType: "bash" @@ -86,9 +81,66 @@ stages: export GOOGLE_OAUTH_ACCESS_TOKEN=$ACCESS_TOKEN export CLOUDSDK_AUTH_ACCESS_TOKEN=$ACCESS_TOKEN - export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=$(serviceaccount)@$(projectID).iam.gserviceaccount.com + export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=$(serviceaccount)@$(projectID).iam.gserviceaccount.com gcloud config set project $(projectID) + terraform init - terraform validate - terraform plan - terraform apply --auto-approve \ No newline at end of file + + terraform plan -out=test.tfplan + + terraform show -json ./test.tfplan > ./tfplan.json + + - task: AzureCLI@2 + displayName: "Policy Validate" + condition: eq(variables.policyValidate,'true') + inputs: + azureSubscription: "$(azureSubscription)" + scriptType: "bash" + scriptLocation: "inlineScript" + workingDirectory: "examples/guardrails/folder-factory" + inlineScript: | + + export GOOGLE_OAUTH_ACCESS_TOKEN=$ACCESS_TOKEN + export CLOUDSDK_AUTH_ACCESS_TOKEN=$ACCESS_TOKEN + export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=$(serviceaccount)@$(projectID).iam.gserviceaccount.com + gcloud config set project $(projectID) + + echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list + curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - + + sudo apt-get update + sudo apt-get install -y google-cloud-sdk-terraform-tools + + git clone https://github.com/GoogleCloudPlatform/policy-library.git ./policy-library + + cd ./policy-library + #&& cp samples/iam_service_accounts_only.yaml policies/constraints + + gcloud beta terraform vet ../tfplan.json --policy-library=. --format=json + violations=$(gcloud beta terraform vet ../tfplan.json --policy-library=. --format=json) + ret_val=$? + if [ $ret_val -eq 2 ]; then + echo "$violations" + echo "Violations found, not proceeding with terraform apply" + exit 1 + elif [ $ret_val -ne 0 ]; then + echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" + exit 1 + else + echo "No policy violations detected; proceeding with terraform apply" + fi + + - task: AzureCLI@2 + displayName: "Terraform apply" + inputs: + azureSubscription: "$(azureSubscription)" + scriptType: "bash" + scriptLocation: "inlineScript" + workingDirectory: "examples/guardrails/folder-factory" + inlineScript: | + + export GOOGLE_OAUTH_ACCESS_TOKEN=$ACCESS_TOKEN + export CLOUDSDK_AUTH_ACCESS_TOKEN=$ACCESS_TOKEN + export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=$(serviceaccount)@$(projectID).iam.gserviceaccount.com + gcloud config set project $(projectID) + terraform apply -auto-approve ./test.tfplan \ No newline at end of file diff --git a/examples/guardrails/azuredevops/project-factory/azure-pipeline.yml b/examples/guardrails/azuredevops/project-factory/azure-pipeline.yml index 083d4ee..6f2fe00 100644 --- a/examples/guardrails/azuredevops/project-factory/azure-pipeline.yml +++ b/examples/guardrails/azuredevops/project-factory/azure-pipeline.yml @@ -3,16 +3,17 @@ name: "$(Date:yyyyMMdd)$(Rev:.r)" trigger: branches: include: - - main + - none variables: - azureSubscription: "" - projectID: "" - workloadIdentityPoolProvider: "" - Projectnumber: "" - serviceaccount: "" - workloadIdentityPools: "" + azureSubscription: "azuretogcp" + projectID: "azuretogcp-374609" + workloadIdentityPoolProvider: "azure" + Projectnumber: "878833087983" + serviceaccount: "azure-sa" + workloadIdentityPools: "azure" + policyValidate: "true" pool: @@ -23,7 +24,7 @@ stages: - stage: auth displayName: "GCP WIF Auth" jobs: - - job: governance_pipeline + - job: governance_pipeline_azure timeoutInMinutes: 30 steps: - task: charleszipp.azure-pipelines-tasks-terraform.azure-pipelines-tasks-terraform-installer.TerraformInstaller@0 @@ -67,33 +68,79 @@ stages: echo "##vso[task.setvariable variable=ACCESS_TOKEN]$ACCESS_TOKEN" echo $ACCESS_TOKEN - - - task: downloadgitlabrepository@1 - displayName: "download repo" - inputs: - connection: 'Gauravgitlab' - definition: '43337570' - versionSelector: 'latestDefaultBranch' + - task: AzureCLI@2 - displayName: project-factory + displayName: "Terraform plan" inputs: - azureSubscription: 'azuretogcp' - scriptType: 'bash' - scriptLocation: 'inlineScript' - workingDirectory: 'examples/guardrails/project-factory' + azureSubscription: "$(azureSubscription)" + scriptType: "bash" + scriptLocation: "inlineScript" + workingDirectory: "examples/guardrails/project-factory" inlineScript: | - - export GOOGLE_OAUTH_ACCESS_TOKEN=$ACCESS_TOKEN + export GOOGLE_OAUTH_ACCESS_TOKEN=$ACCESS_TOKEN export CLOUDSDK_AUTH_ACCESS_TOKEN=$ACCESS_TOKEN - - export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=$(serviceaccount)@$(projectID).iam.gserviceaccount.com - + export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=$(serviceaccount)@$(projectID).iam.gserviceaccount.com gcloud config set project $(projectID) - + terraform init + + terraform plan -out=test.tfplan + + terraform show -json ./test.tfplan > ./tfplan.json - terraform plan + - task: AzureCLI@2 + displayName: "Policy Validate" + condition: eq(variables.policyValidate,'true') + inputs: + azureSubscription: "$(azureSubscription)" + scriptType: "bash" + scriptLocation: "inlineScript" + workingDirectory: "examples/guardrails/project-factory" + inlineScript: | + + export GOOGLE_OAUTH_ACCESS_TOKEN=$ACCESS_TOKEN + export CLOUDSDK_AUTH_ACCESS_TOKEN=$ACCESS_TOKEN + export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=$(serviceaccount)@$(projectID).iam.gserviceaccount.com + gcloud config set project $(projectID) + + echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list + curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - + + sudo apt-get update + sudo apt-get install -y google-cloud-sdk-terraform-tools - terraform apply --auto-approve \ No newline at end of file + git clone https://github.com/GoogleCloudPlatform/policy-library.git ./policy-library + + cd ./policy-library + #&& cp samples/iam_service_accounts_only.yaml policies/constraints + + gcloud beta terraform vet ../tfplan.json --policy-library=. --format=json + violations=$(gcloud beta terraform vet ../tfplan.json --policy-library=. --format=json) + ret_val=$? + if [ $ret_val -eq 2 ]; then + echo "$violations" + echo "Violations found, not proceeding with terraform apply" + exit 1 + elif [ $ret_val -ne 0 ]; then + echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" + exit 1 + else + echo "No policy violations detected; proceeding with terraform apply" + fi + + - task: AzureCLI@2 + displayName: "Terraform apply" + inputs: + azureSubscription: "$(azureSubscription)" + scriptType: "bash" + scriptLocation: "inlineScript" + workingDirectory: "examples/guardrails/project-factory" + inlineScript: | + + export GOOGLE_OAUTH_ACCESS_TOKEN=$ACCESS_TOKEN + export CLOUDSDK_AUTH_ACCESS_TOKEN=$ACCESS_TOKEN + export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=$(serviceaccount)@$(projectID).iam.gserviceaccount.com + gcloud config set project $(projectID) + terraform apply -auto-approve ./test.tfplan \ No newline at end of file diff --git a/examples/guardrails/azuredevops/skunkworks/With VPC Service Controls, you can defin.md b/examples/guardrails/azuredevops/skunkworks/With VPC Service Controls, you can defin.md new file mode 100644 index 0000000..fa3e38c --- /dev/null +++ b/examples/guardrails/azuredevops/skunkworks/With VPC Service Controls, you can defin.md @@ -0,0 +1,3 @@ +With VPC Service Controls, you can define policies that restrict the traffic that can flow between your Google Cloud resources and other Google services, including third-party services. + +Let's say you have a Google Cloud project that contains sensitive data, such as personally identifiable information (PII) or financial data. You want to make sure that this data is protected from unauthorized access or exfiltration, to achieve this, you can use VPC Service Controls to create a security perimeter around your project like you can create a policy that allows your project to access specific Google services, such as Google Cloud Storage or Google Cloud SQL, but blocks access to other services that are not necessary for your project. You can also create a policy that requires all traffic to or from your project to be encrypted or authenticated using specific protocols or methods. \ No newline at end of file diff --git a/examples/guardrails/azuredevops/skunkworks/azure-pipeline.yml b/examples/guardrails/azuredevops/skunkworks/azure-pipeline.yml index 47d78b8..2bc6cb6 100644 --- a/examples/guardrails/azuredevops/skunkworks/azure-pipeline.yml +++ b/examples/guardrails/azuredevops/skunkworks/azure-pipeline.yml @@ -3,16 +3,17 @@ name: "$(Date:yyyyMMdd)$(Rev:.r)" trigger: branches: include: - - main + - none variables: - azureSubscription: "" - projectID: "" - workloadIdentityPoolProvider: "" - Projectnumber: "" - serviceaccount: "" - workloadIdentityPools: "" + azureSubscription: "azuretogcp" + projectID: "azuretogcp-374609" + workloadIdentityPoolProvider: "azure" + Projectnumber: "878833087983" + serviceaccount: "azure-sa" + workloadIdentityPools: "azure" + policyValidate: "true" pool: @@ -23,7 +24,7 @@ stages: - stage: auth displayName: "GCP WIF Auth" jobs: - - job: governance_pipeline + - job: governance_pipeline_azure timeoutInMinutes: 30 steps: - task: charleszipp.azure-pipelines-tasks-terraform.azure-pipelines-tasks-terraform-installer.TerraformInstaller@0 @@ -67,31 +68,79 @@ stages: echo "##vso[task.setvariable variable=ACCESS_TOKEN]$ACCESS_TOKEN" echo $ACCESS_TOKEN - - - task: downloadgitlabrepository@1 - displayName: "download repo" - inputs: - connection: 'Gauravgitlab' - definition: '43337570' - versionSelector: 'latestDefaultBranch' + - task: AzureCLI@2 - displayName: skunkworks + displayName: "Terraform plan" inputs: - azureSubscription: 'azuretogcp' - scriptType: 'bash' - scriptLocation: 'inlineScript' - workingDirectory: 'examples/guardrails/skunkworks' + azureSubscription: "$(azureSubscription)" + scriptType: "bash" + scriptLocation: "inlineScript" + workingDirectory: "examples/guardrails/skunkworks" inlineScript: | - - export GOOGLE_OAUTH_ACCESS_TOKEN=$ACCESS_TOKEN + export GOOGLE_OAUTH_ACCESS_TOKEN=$ACCESS_TOKEN export CLOUDSDK_AUTH_ACCESS_TOKEN=$ACCESS_TOKEN + export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=$(serviceaccount)@$(projectID).iam.gserviceaccount.com + gcloud config set project $(projectID) + + terraform init + + terraform plan -out=test.tfplan + + terraform show -json ./test.tfplan > ./tfplan.json - export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=$(serviceaccount)@$(projectID).iam.gserviceaccount.com + - task: AzureCLI@2 + displayName: "Policy Validate" + condition: eq(variables.policyValidate,'true') + inputs: + azureSubscription: "$(azureSubscription)" + scriptType: "bash" + scriptLocation: "inlineScript" + workingDirectory: "examples/guardrails/skunkworks" + inlineScript: | - gcloud config set project $(projectID) + export GOOGLE_OAUTH_ACCESS_TOKEN=$ACCESS_TOKEN + export CLOUDSDK_AUTH_ACCESS_TOKEN=$ACCESS_TOKEN + export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=$(serviceaccount)@$(projectID).iam.gserviceaccount.com + gcloud config set project $(projectID) + + echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list + curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - + + sudo apt-get update + sudo apt-get install -y google-cloud-sdk-terraform-tools + + git clone https://github.com/GoogleCloudPlatform/policy-library.git ./policy-library + + cd ./policy-library + #&& cp samples/iam_service_accounts_only.yaml policies/constraints + + gcloud beta terraform vet ../tfplan.json --policy-library=. --format=json + violations=$(gcloud beta terraform vet ../tfplan.json --policy-library=. --format=json) + ret_val=$? + if [ $ret_val -eq 2 ]; then + echo "$violations" + echo "Violations found, not proceeding with terraform apply" + exit 1 + elif [ $ret_val -ne 0 ]; then + echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" + exit 1 + else + echo "No policy violations detected; proceeding with terraform apply" + fi + + - task: AzureCLI@2 + displayName: "Terraform apply" + inputs: + azureSubscription: "$(azureSubscription)" + scriptType: "bash" + scriptLocation: "inlineScript" + workingDirectory: "examples/guardrails/skunkworks" + inlineScript: | - terraform init - terraform plan - terraform apply --auto-approve \ No newline at end of file + export GOOGLE_OAUTH_ACCESS_TOKEN=$ACCESS_TOKEN + export CLOUDSDK_AUTH_ACCESS_TOKEN=$ACCESS_TOKEN + export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=$(serviceaccount)@$(projectID).iam.gserviceaccount.com + gcloud config set project $(projectID) + terraform apply -auto-approve ./test.tfplan \ No newline at end of file From 31fe9e16cd16adb90c21096dff30df67b5b72f45 Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Mon, 20 Mar 2023 23:31:14 +0530 Subject: [PATCH 121/132] azure devops config validator step added --- .../azuredevops/folder-factory/README.md | 79 ++++++++++++++----- .../azuredevops/project-factory/README.md | 78 ++++++++++++------ .../azuredevops/skunkworks/README.md | 66 ++++++++++++---- 3 files changed, 161 insertions(+), 62 deletions(-) diff --git a/examples/guardrails/azuredevops/folder-factory/README.md b/examples/guardrails/azuredevops/folder-factory/README.md index a139948..58cbb56 100644 --- a/examples/guardrails/azuredevops/folder-factory/README.md +++ b/examples/guardrails/azuredevops/folder-factory/README.md @@ -4,31 +4,12 @@ This is a template for a DevOps folder factory. It can be used with [https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory](https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory) and is intended to house the folder configurations: -![Screenshot 2022-05-10 12 00 19 PM](https://user-images.githubusercontent.com/94000358/169809437-aaa8538e-3ffc-48b3-9028-84e4995de150.png) +Screenshot 2023-03-10 at 02 52 08 Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. -## Repository Configuration -This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. - -If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw - -After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: - -Secret configuration - -or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: -``` -env: - STATE_BUCKET: 'XXXX' - # The GCS bucket to store the terraform state - WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' - # The workload identity provider that should be used for this repository. - SERVICE_ACCOUNT: 'XXXX@XXXX' - # The service account that should be used for this repository. -``` ## Setting up folders @@ -56,4 +37,60 @@ iam: - serviceAccount:XXXXX@XXXXXX ``` -Every folder is defined with its own yaml file located in the following [Folder](data/folder). +Every folder is defined with its own yaml file located in the following [Folder](data/folders). +Copy "folder.yaml.sample" to "folder_name.yaml"; Name of the yaml file will be used to create folder with the same name. +Once folder_name.yaml file is created update yaml file + * parent - can be another folder or organization + * ServiceAccount +data/folders can have multiple yaml files and a folder will be created for each yaml file. + + +## How to run this stage +### Prerequisites + +Workload Identity setup between the folder factory code repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. There is a sample code provided in “folder.yaml.sample” to create a folder and for terraform to create a folder minimum below permissions are required. +“Folder Creator” or “Folder Admin” at org level +“Organization Policy Admin” at org level + + +### Installation Steps +From the folder-factory edit the azure-pipeline.yml + +* CI/CD variables + + Add the variables to the pipeline as described in the table below. + + +### Terraform config validator +The pipeline has an option to utilise the integrated config validator (gcloud terraform vet) to impose constraints on your terraform configuration. You can enable it by setting the CI/CD Variable $TERRAFORM_POLICY_VALIDATE to "true" and providing the policy-library repo URL to $POLICY_LIBRARY_REPO variable. See the below for details on the Variables to be set on the CI/CD pipeline. + + +| Variable | Description |Sample value | +|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| GCP_PROJECT_ID | The GCP project ID of your service account | sample-project-1122 | +| GCP_SERVICE_ACCOUNT | The Service Account to be used for creating folders | xyz@sample-project-1122.iam.gserviceaccount.com | +| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_NAME}/providers/${PROVIDER_NAME} | +| STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket. Use a seed project if running this as part of Foundations or create a new GCS Bucket. | sample-terraform-state-bucket | +| TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | +| TF_ROOT | The directory of the terraform code to be executed. | $CI_PROJECT_DIR | +| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 +| TERRAFORM_POLICY_VALIDATE | Set this value as true if terraform vet is to be run against the policy library repository set in $POLICY_LIBRARY_REPO variable | true | +| POLICY_LIBRARY_REPO | The policy library repository URL which will be cloned using git clone to run gcloud terraform vet against. | https://github.com/GoogleCloudPlatform/policy-library + +* Once the prerequisites are set up, trigger the pipeline manually or set the trigger as per your need. + + +* .gcp-auth script should run successfully in the pipeline if the workload identity federation is configured as required. + +### Pipeline Workflow Overview +The complete workflow comprises of 4-5 stages + * Stages: + * gcp-auth : creates the wif credentials by impersonating the service account. + * terraform init : initializes terraform in the specified TF_ROOT directory + * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages + * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command + * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts + * policy-validate: Runs gcloud terraform vet against the terraform code with the constraints in the specified repository. + * apply: once the plan is successful. + Runs terraform apply and creates the infrastructure specified. + diff --git a/examples/guardrails/azuredevops/project-factory/README.md b/examples/guardrails/azuredevops/project-factory/README.md index f901e7f..aef58d4 100644 --- a/examples/guardrails/azuredevops/project-factory/README.md +++ b/examples/guardrails/azuredevops/project-factory/README.md @@ -2,37 +2,16 @@ This is a template for a DevOps project factory. -It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory) and is intended to house the projects of a specified folder: +It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/gitlab/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/gitlab/folder-factory) and is intended to house the projects of a specified folder: Overview Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. -![Folder Factory](https://user-images.githubusercontent.com/94000358/169809882-f5ff9fb1-d037-49de-8c2c-bf0d457b662f.png) +Screenshot 2023-03-10 at 02 53 10 The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. -## Repository Configuration -This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. - -If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw - -After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: - -Secret settings - -or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: -``` -env: - STATE_BUCKET: 'XXXX' - # The GCS bucket to store the terraform state - FOLDER: 'folders/XXXX' - # The folder under which the projects should be created - WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' - # The workload identity provider that should be used for this repository. - SERVICE_ACCOUNT: 'XXXX@XXXX' - # The service account that should be used for this repository. -``` ## Setting up projects @@ -67,9 +46,60 @@ roles: - roles/storage.objectAdmin - roles/storage.objectCreator - roles/storage.objectViewer -repo_provider: github +repo_provider: gitlab repo_name: devops-governance/skunkworks repo_branch: dev ``` Every project is defined with its own file located in the [Project Folder](data/projects). + +## How to run this stage + +### Prerequisites +The parent folders are provisioned to place the projects. + + +Workload Identity setup between the project factory repositories and the GCP Identity provider configured with a service account containing required permissions to create projects, workload identity pools and providers, service accounts and IAM bindings on the service accounts under the parent folder in which the projects are to be created. +“Project Creator” should already be granted when running the folder factory. +“Billing User” on Billing Account + +### Installation Steps +From the project-factory repo +CICD configuration file path +Navigate to the azure-pipeline.yml file + +CI/CD variables +Add the variables to the pipeline as described in the table below. + +### Terraform config validator +The pipeline has an option to utilise the integrated config validator (gcloud terraform vet) to impose constraints on your terraform configuration. You can enable it by setting the CI/CD Variable $TERRAFORM_POLICY_VALIDATE to "true" and providing the policy-library repo URL to $POLICY_LIBRARY_REPO variable. See the below for details on the Variables to be set on the CI/CD pipeline. + +| Variable | Description | Sample value | +|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| GCP_PROJECT_ID | The GCP project ID of your service account | sample-project-1122 | +| GCP_SERVICE_ACCOUNT | The Service Account to be used for creating projects | xyz@sample-project-1122.iam.gserviceaccount.com | +| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_NAME}/providers/${PROVIDER_NAME} | +| STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | +| TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | +| TF_ROOT | The directory of the terraform code to be executed. | $CI_PROJECT_DIR | +| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 | +| TERRAFORM_POLICY_VALIDATE | Set this value as true if terraform vet is to be run against the policy library repository set in $POLICY_LIBRARY_REPO variable | true | +| POLICY_LIBRARY_REPO | The policy library repository URL which will be cloned using git clone to run gcloud terraform vet against. | https://github.com/GoogleCloudPlatform/policy-library | + +Similar to Folder factory, + +Once the prerequisites are set up, manually trigger the pipeline. + +.gcp-auth script should run successfully in the pipeline if the workload identity federation is configured as required. + +### Pipeline Workflow Overview +The complete workflow comprises of 4-5 stages and 2 before-script jobs +* Stages: + * gcp-auth : creates the wif credentials by impersonating the service account. + * terraform init : initializes terraform in the specified TF_ROOT directory + * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages + * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command + * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts + * policy-validate: Runs gcloud terraform vet against the terraform code with the constraints in the specified repository. + * apply: once the plan is successful. + Runs terraform apply and creates the infrastructure specified. \ No newline at end of file diff --git a/examples/guardrails/azuredevops/skunkworks/README.md b/examples/guardrails/azuredevops/skunkworks/README.md index 0b89bb2..6caff47 100644 --- a/examples/guardrails/azuredevops/skunkworks/README.md +++ b/examples/guardrails/azuredevops/skunkworks/README.md @@ -2,28 +2,60 @@ This is a template for an IaC kickstarter repository. -![Skunkworks](https://user-images.githubusercontent.com/94000358/169810982-36f01de2-e5e5-4ecd-b98e-3cf5a6aa9f81.png) +Screenshot 2023-03-10 at 02 53 38 -The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. +The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on AzuredevopsAzure. It is based on the following "ideal" pipeline: + +![Gitlab](https://user-images.githubusercontent.com/94000358/224205000-7cfb0fe0-6520-421b-88bd-ba7efb20ffd4.png) This template creates a bucket in the specified target environment. -## Repository Configuration -This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. +## How to run this stage + +### Prerequisites +Project factory is executed successfully and the respective service accounts for all the environments and projects are in place. + + +The branch structure should mirror the environments that are going to be deployed. For example, for deploying resources in dev, staging and prod skunkworks projects, three protected branches for dev, staging and prod are required. + + +### Installation Steps +1. Update the CICD configuration file path in the repository + * From the skunkworks ADO repo , update CI/CD configuration file value to the relative path of the azure-pipeline.yml file + +2. Update the CI/CD variables + * From the skunkworks repo, Add the below variables to the pipeline + +### Terraform config validator +The pipeline has an option to utilise the integrated config validator (gcloud terraform vet) to impose constraints on your terraform configuration. You can enable it by setting the CI/CD Variable $TERRAFORM_POLICY_VALIDATE to "true" and providing the policy-library repo URL to $POLICY_LIBRARY_REPO variable. See the below for details on the Variables to be set on the CI/CD pipeline. -If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw -After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: +| Variable | Description | Sample value | +|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| DEV_GCP_PROJECT_ID | The GCP project ID in which resources are to be created on a push event to dev branch | sample-dev-project-1122 | +| DEV_GCP_SERVICE_ACCOUNT | The Service Account of the dev gcp project configured with Workload Identity Federation (WIF) | xyz@sample-dev-project-1122.iam.gserviceaccount.com | +| STAGE_GCP_PROJECT_ID | The GCP project ID in which resources are to be created on a push event to staging branch | sample-stage-project-1122 | +| STAGE_GCP_SERVICE_ACCOUNT | The Service Account of the staging gcp project configured with Workload Identity Federation (WIF) | xyz@sample-stage-project-1122.iam.gserviceaccount.com | +| PROD_GCP_PROJECT_ID | The GCP project ID in which resources are to be created on a push event to prod branch | sample-prod-project-1122 | +| PROD_GCP_SERVICE_ACCOUNT | The Service Account of the prod gcp project configured with Workload Identity Federation (WIF) | xyz@sample-prod-project-1122.iam.gserviceaccount.com | +| GCP_WORKLOAD_IDENTITY_PROVIDER | The Workload Identity provider URI configured with the Service Account and the repository | projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_NAME}/providers/${PROVIDER_NAME} | +| STATE_BUCKET | The GCS bucket in which the state is to be centrally managed. The Service account provided above must have access to list and write files to this bucket | sample-terraform-state-bucket | +| TF_LOG | The terraform env variable setting to get detailed logs. Supports TRACE,DEBUG,INFO,WARN,ERROR in order of decreasing verbosity | WARN | +| TF_ROOT | The directory of the terraform code to be executed. variables | $CI_PROJECT_DIR | +| TF_VERSION | The terraform version to be used for execution. The specified terraform version is downloaded and used for execution for the workflow. | 1.3.6 +| TERRAFORM_POLICY_VALIDATE | Set this value as true if terraform vet is to be run against the policy library repository set in $POLICY_LIBRARY_REPO | true | +| POLICY_LIBRARY_REPO | The policy library repository URL which will be cloned using git clone to run gcloud terraform vet against. | https://github.com/GoogleCloudPlatform/policy-library | | -Secret configuration +## Pipeline Workflow Overview +The complete workflow contains a parent child pipeline. The parent(azure-pipeline.yaml) file is the trigger stage for each of the environments. It passes relevant variables for that environment to the child pipeline which executes the core terraform workflow. The child pipeline workflow executes 4-5 stages and 2 before-script jobs -or by modifing the [Workflow Action Files](.github/workflows/) and setting the environment variables: -``` -env: - STATE_BUCKET: 'XXXX' - # The GCS bucket to store the terraform state - WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' - # The workload identity provider that should be used for this repository. - SERVICE_ACCOUNT: 'XXXX@XXXX' - # The service account that should be used for this repository. -``` +* before_script jobs : + * gcp-auth : creates the wif credentials by impersonating the service account. + * terraform init : initializes terraform in the specified TF_ROOT directory +* Stages: + * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages + * validate: Runs terraform fmt check and terraform validate. This stage fails if the code is not run against terraform fmt command + * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts + * policy-validate: Runs gcloud terraform vet against the terraform code with the constraints in the specified repository. + * apply: once the plan is successful. + Runs terraform apply and creates the infrastructure specified. From 2261a760b198595252eed4a2c640e7dee421fdf0 Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Thu, 23 Mar 2023 13:49:33 +0530 Subject: [PATCH 122/132] updated README --- .../folder-factory/azure-pipeline.yml | 22 +++++++++---------- .../project-factory/azure-pipeline.yml | 22 +++++++++---------- .../azuredevops/skunkworks/azure-pipeline.yml | 22 +++++++++---------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml b/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml index 108ef15..e7e0a80 100644 --- a/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml +++ b/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml @@ -7,13 +7,13 @@ trigger: variables: - azureSubscription: "azuretogcp" - projectID: "azuretogcp-374609" - workloadIdentityPoolProvider: "azure" - Projectnumber: "878833087983" - serviceaccount: "azure-sa" - workloadIdentityPools: "azure" - policyValidate: "true" + azureserviceconnection: "service connection name between azure devops and gcp" + projectID: "gcp project ID" + workloadIdentityPoolProvider: "pool provider in gcp wif" + Projectnumber: "gcp project no" + serviceaccount: "sa in gcp" + workloadIdentityPools: "pool in gcp wif" + policyValidate: "true/false" pool: @@ -36,7 +36,7 @@ stages: - task: AzureCLI@2 displayName: "Get access token" inputs: - azureSubscription: "$(azureSubscription)" + azureserviceconnection: "$(azureserviceconnection)" scriptType: "bash" scriptLocation: "inlineScript" inlineScript: | @@ -73,7 +73,7 @@ stages: - task: AzureCLI@2 displayName: "Terraform plan" inputs: - azureSubscription: "$(azureSubscription)" + azureserviceconnection: "$(azureserviceconnection)" scriptType: "bash" scriptLocation: "inlineScript" workingDirectory: "examples/guardrails/folder-factory" @@ -94,7 +94,7 @@ stages: displayName: "Policy Validate" condition: eq(variables.policyValidate,'true') inputs: - azureSubscription: "$(azureSubscription)" + azureserviceconnection: "$(azureserviceconnection)" scriptType: "bash" scriptLocation: "inlineScript" workingDirectory: "examples/guardrails/folder-factory" @@ -133,7 +133,7 @@ stages: - task: AzureCLI@2 displayName: "Terraform apply" inputs: - azureSubscription: "$(azureSubscription)" + azureserviceconnection: "$(azureserviceconnection)" scriptType: "bash" scriptLocation: "inlineScript" workingDirectory: "examples/guardrails/folder-factory" diff --git a/examples/guardrails/azuredevops/project-factory/azure-pipeline.yml b/examples/guardrails/azuredevops/project-factory/azure-pipeline.yml index 6f2fe00..0c1c9d1 100644 --- a/examples/guardrails/azuredevops/project-factory/azure-pipeline.yml +++ b/examples/guardrails/azuredevops/project-factory/azure-pipeline.yml @@ -7,13 +7,13 @@ trigger: variables: - azureSubscription: "azuretogcp" - projectID: "azuretogcp-374609" - workloadIdentityPoolProvider: "azure" - Projectnumber: "878833087983" - serviceaccount: "azure-sa" - workloadIdentityPools: "azure" - policyValidate: "true" + azureserviceconnection: "service connection name between azure devops and gcp" + projectID: "gcp project ID" + workloadIdentityPoolProvider: "pool provider in gcp wif" + Projectnumber: "gcp project no" + serviceaccount: "sa in gcp" + workloadIdentityPools: "pool in gcp wif" + policyValidate: "true/false" pool: @@ -36,7 +36,7 @@ stages: - task: AzureCLI@2 displayName: "Get access token" inputs: - azureSubscription: "$(azureSubscription)" + azureserviceconnection: "$(azureserviceconnection)" scriptType: "bash" scriptLocation: "inlineScript" inlineScript: | @@ -73,7 +73,7 @@ stages: - task: AzureCLI@2 displayName: "Terraform plan" inputs: - azureSubscription: "$(azureSubscription)" + azureserviceconnection: "$(azureserviceconnection)" scriptType: "bash" scriptLocation: "inlineScript" workingDirectory: "examples/guardrails/project-factory" @@ -94,7 +94,7 @@ stages: displayName: "Policy Validate" condition: eq(variables.policyValidate,'true') inputs: - azureSubscription: "$(azureSubscription)" + azureserviceconnection: "$(azureserviceconnection)" scriptType: "bash" scriptLocation: "inlineScript" workingDirectory: "examples/guardrails/project-factory" @@ -133,7 +133,7 @@ stages: - task: AzureCLI@2 displayName: "Terraform apply" inputs: - azureSubscription: "$(azureSubscription)" + azureserviceconnection: "$(azureserviceconnection)" scriptType: "bash" scriptLocation: "inlineScript" workingDirectory: "examples/guardrails/project-factory" diff --git a/examples/guardrails/azuredevops/skunkworks/azure-pipeline.yml b/examples/guardrails/azuredevops/skunkworks/azure-pipeline.yml index 2bc6cb6..628a479 100644 --- a/examples/guardrails/azuredevops/skunkworks/azure-pipeline.yml +++ b/examples/guardrails/azuredevops/skunkworks/azure-pipeline.yml @@ -7,13 +7,13 @@ trigger: variables: - azureSubscription: "azuretogcp" - projectID: "azuretogcp-374609" - workloadIdentityPoolProvider: "azure" - Projectnumber: "878833087983" - serviceaccount: "azure-sa" - workloadIdentityPools: "azure" - policyValidate: "true" + azureserviceconnection: "service connection name between azure devops and gcp" + projectID: "gcp project ID" + workloadIdentityPoolProvider: "pool provider in gcp wif" + Projectnumber: "gcp project no" + serviceaccount: "sa in gcp" + workloadIdentityPools: "pool in gcp wif" + policyValidate: "true/false" pool: @@ -36,7 +36,7 @@ stages: - task: AzureCLI@2 displayName: "Get access token" inputs: - azureSubscription: "$(azureSubscription)" + azureserviceconnection: "$(azureserviceconnection)" scriptType: "bash" scriptLocation: "inlineScript" inlineScript: | @@ -73,7 +73,7 @@ stages: - task: AzureCLI@2 displayName: "Terraform plan" inputs: - azureSubscription: "$(azureSubscription)" + azureserviceconnection: "$(azureserviceconnection)" scriptType: "bash" scriptLocation: "inlineScript" workingDirectory: "examples/guardrails/skunkworks" @@ -94,7 +94,7 @@ stages: displayName: "Policy Validate" condition: eq(variables.policyValidate,'true') inputs: - azureSubscription: "$(azureSubscription)" + azureserviceconnection: "$(azureserviceconnection)" scriptType: "bash" scriptLocation: "inlineScript" workingDirectory: "examples/guardrails/skunkworks" @@ -133,7 +133,7 @@ stages: - task: AzureCLI@2 displayName: "Terraform apply" inputs: - azureSubscription: "$(azureSubscription)" + azureserviceconnection: "$(azureserviceconnection)" scriptType: "bash" scriptLocation: "inlineScript" workingDirectory: "examples/guardrails/skunkworks" From 8a58d40bad30583408ae0ccbefa7698650975873 Mon Sep 17 00:00:00 2001 From: agutta Date: Thu, 23 Mar 2023 16:44:45 +0530 Subject: [PATCH 123/132] Update README.md --- examples/guardrails/azuredevops/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/guardrails/azuredevops/README.md b/examples/guardrails/azuredevops/README.md index c09f4bc..3d737b4 100644 --- a/examples/guardrails/azuredevops/README.md +++ b/examples/guardrails/azuredevops/README.md @@ -13,7 +13,7 @@ To use Azure Devops with GCP Deployments, we can leverage on [Workload Identity ## ADO Prerequisites - Create a Project in ADO -- Add a repo called "folder factory" and copy code from [devops folder factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/folder-factory) into it +- Add a repo called "folder factory" and copy code from [devops folder factory repo](../../../examples/guardrails/azuredevops/folder-factory) into it - Add repo called "project factory" and copy code from [devops project factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/project-factory) into it - Add a repo called "skunkworks" and copy code from [devops skunkworks repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/skunkworks) into it. From 9bd3873df7cf3351745fd4510bec127c3a1af2ec Mon Sep 17 00:00:00 2001 From: agutta Date: Thu, 23 Mar 2023 16:45:42 +0530 Subject: [PATCH 124/132] Update README.md --- examples/guardrails/azuredevops/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/guardrails/azuredevops/README.md b/examples/guardrails/azuredevops/README.md index 3d737b4..4e8b9e9 100644 --- a/examples/guardrails/azuredevops/README.md +++ b/examples/guardrails/azuredevops/README.md @@ -14,8 +14,8 @@ To use Azure Devops with GCP Deployments, we can leverage on [Workload Identity - Create a Project in ADO - Add a repo called "folder factory" and copy code from [devops folder factory repo](../../../examples/guardrails/azuredevops/folder-factory) into it -- Add repo called "project factory" and copy code from [devops project factory repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/project-factory) into it -- Add a repo called "skunkworks" and copy code from [devops skunkworks repo](https://github.com/google/devops-governance/tree/GDC-phase-kickstarter-1/examples/guardrails/gitlab/skunkworks) into it. +- Add repo called "project factory" and copy code from [devops project factory repo](../../../examples/guardrails/gitlab/project-factory) into it +- Add a repo called "skunkworks" and copy code from [devops skunkworks repo](../../../examples/guardrails/gitlab/skunkworks) into it. > .\ -Once ADO set is completed go to [Folder Factory](https://github.com/google/devops-governance/blob/GDC-phase-kickstarter-1/examples/guardrails/gitlab/folder-factory) +Once ADO set is completed go to [Folder Factory](../../../examples/guardrails/gitlab/folder-factory) From eab44d9911b148d259ed18767f140c952227596e Mon Sep 17 00:00:00 2001 From: agutta Date: Thu, 23 Mar 2023 16:46:08 +0530 Subject: [PATCH 125/132] Update README.md --- examples/guardrails/azuredevops/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/guardrails/azuredevops/README.md b/examples/guardrails/azuredevops/README.md index 4e8b9e9..1eab061 100644 --- a/examples/guardrails/azuredevops/README.md +++ b/examples/guardrails/azuredevops/README.md @@ -14,8 +14,8 @@ To use Azure Devops with GCP Deployments, we can leverage on [Workload Identity - Create a Project in ADO - Add a repo called "folder factory" and copy code from [devops folder factory repo](../../../examples/guardrails/azuredevops/folder-factory) into it -- Add repo called "project factory" and copy code from [devops project factory repo](../../../examples/guardrails/gitlab/project-factory) into it -- Add a repo called "skunkworks" and copy code from [devops skunkworks repo](../../../examples/guardrails/gitlab/skunkworks) into it. +- Add repo called "project factory" and copy code from [devops project factory repo](../../../examples/guardrails/azuredevops/project-factory) into it +- Add a repo called "skunkworks" and copy code from [devops skunkworks repo](../../../examples/guardrails/azuredevops/skunkworks) into it. > .\ -Once ADO set is completed go to [Folder Factory](../../../examples/guardrails/gitlab/folder-factory) +Once ADO set is completed go to [Folder Factory](../../../examples/guardrails/azuredevops/folder-factory) From 2493b8887078d922edd5294ede5640d9b38b1bea Mon Sep 17 00:00:00 2001 From: Shubham Singh Date: Fri, 24 Mar 2023 15:16:01 +0530 Subject: [PATCH 126/132] add custom handle for violations --- .../.bitbucket/bitbucket-pipelines.yml | 9 ++++--- .../.bitbucket/bitbucket-pipelines.yml | 9 ++++--- .../.bitbucket/bitbucket-pipelines.yml | 27 ++++++++++++------- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml index 2b9262c..aa6ac7e 100644 --- a/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml +++ b/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml @@ -65,9 +65,12 @@ pipelines: - ret_val=$? - if [ $ret_val -eq 2 ]; - then - - echo "$violations" - - echo "Violations found, not proceeding with terraform apply" - - exit 1 + - if [ "$violations" != "[]" ] ; + - then + - echo "$violations" + - echo "Violations found, not proceeding with terraform apply" + - exit 1 + - fi - elif [ $ret_val -ne 0 ]; - then - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" diff --git a/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml index 2170db0..4bb1940 100644 --- a/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml +++ b/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml @@ -65,9 +65,12 @@ pipelines: - ret_val=$? - if [ $ret_val -eq 2 ]; - then - - echo "$violations" - - echo "Violations found, not proceeding with terraform apply" - - exit 1 + - if [ "$violations" != "[]" ] ; + - then + - echo "$violations" + - echo "Violations found, not proceeding with terraform apply" + - exit 1 + - fi - elif [ $ret_val -ne 0 ]; - then - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" diff --git a/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml index 833d824..1eb2dfe 100644 --- a/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml +++ b/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml @@ -56,9 +56,12 @@ pipelines: - ret_val=$? - if [ $ret_val -eq 2 ]; - then - - echo "$violations" - - echo "Violations found, not proceeding with terraform apply" - - exit 1 + - if [ "$violations" != "[]" ] ; + - then + - echo "$violations" + - echo "Violations found, not proceeding with terraform apply" + - exit 1 + - fi - elif [ $ret_val -ne 0 ]; - then - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" @@ -113,9 +116,12 @@ pipelines: - ret_val=$? - if [ $ret_val -eq 2 ]; - then - - echo "$violations" - - echo "Violations found, not proceeding with terraform apply" - - exit 1 + - if [ "$violations" != "[]" ] ; + - then + - echo "$violations" + - echo "Violations found, not proceeding with terraform apply" + - exit 1 + - fi - elif [ $ret_val -ne 0 ]; - then - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" @@ -170,9 +176,12 @@ pipelines: - ret_val=$? - if [ $ret_val -eq 2 ]; - then - - echo "$violations" - - echo "Violations found, not proceeding with terraform apply" - - exit 1 + - if [ "$violations" != "[]" ] ; + - then + - echo "$violations" + - echo "Violations found, not proceeding with terraform apply" + - exit 1 + - fi - elif [ $ret_val -ne 0 ]; - then - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" From 089e68566295a1e99a3c593aae498aae359d0e21 Mon Sep 17 00:00:00 2001 From: Shubham Singh Date: Sat, 25 Mar 2023 02:25:47 +0530 Subject: [PATCH 127/132] adjust coding style --- .../.bitbucket/bitbucket-pipelines.yml | 17 +++--- .../.bitbucket/bitbucket-pipelines.yml | 15 +++--- .../.bitbucket/bitbucket-pipelines.yml | 53 ++++++++----------- 3 files changed, 35 insertions(+), 50 deletions(-) diff --git a/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml index aa6ac7e..fc1b06b 100644 --- a/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml +++ b/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml @@ -63,16 +63,13 @@ pipelines: - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) - ret_val=$? - - if [ $ret_val -eq 2 ]; - - then - - if [ "$violations" != "[]" ] ; - - then - - echo "$violations" - - echo "Violations found, not proceeding with terraform apply" - - exit 1 - - fi - - elif [ $ret_val -ne 0 ]; - - then + - if [ $ret_val -eq 2 ]; then + - if [ "$violations" != "[]" ] ; then + - echo "$violations" + - echo "Violations found, not proceeding with terraform apply" + - exit 1 + - fi + - elif [ $ret_val -ne 0 ]; then - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" - exit 1 - else diff --git a/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml index 4bb1940..020e2b7 100644 --- a/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml +++ b/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml @@ -63,16 +63,13 @@ pipelines: - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) - ret_val=$? - - if [ $ret_val -eq 2 ]; - - then - - if [ "$violations" != "[]" ] ; - - then - - echo "$violations" - - echo "Violations found, not proceeding with terraform apply" - - exit 1 + - if [ $ret_val -eq 2 ]; then + - if [ "$violations" != "[]" ] ; then + - echo "$violations" + - echo "Violations found, not proceeding with terraform apply" + - exit 1 - fi - - elif [ $ret_val -ne 0 ]; - - then + - elif [ $ret_val -ne 0 ]; then - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" - exit 1 - else diff --git a/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml index 1eb2dfe..7c6c1de 100644 --- a/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml +++ b/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml @@ -54,16 +54,13 @@ pipelines: - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) - ret_val=$? - - if [ $ret_val -eq 2 ]; - - then - - if [ "$violations" != "[]" ] ; - - then - - echo "$violations" - - echo "Violations found, not proceeding with terraform apply" - - exit 1 - - fi - - elif [ $ret_val -ne 0 ]; - - then + - if [ $ret_val -eq 2 ]; then + - if [ "$violations" != "[]" ] ; then + - echo "$violations" + - echo "Violations found, not proceeding with terraform apply" + - exit 1 + - fi + - elif [ $ret_val -ne 0 ]; then - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" - exit 1 - else @@ -113,17 +110,14 @@ pipelines: - /usr/bin/terraform plan -out $TF_PLAN_NAME - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) - - ret_val=$? - - if [ $ret_val -eq 2 ]; - - then - - if [ "$violations" != "[]" ] ; - - then - - echo "$violations" - - echo "Violations found, not proceeding with terraform apply" - - exit 1 - - fi - - elif [ $ret_val -ne 0 ]; - - then + - ret_val=$? + - if [ $ret_val -eq 2 ]; then + - if [ "$violations" != "[]" ] ; then + - echo "$violations" + - echo "Violations found, not proceeding with terraform apply" + - exit 1 + - fi + - elif [ $ret_val -ne 0 ]; then - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" - exit 1 - else @@ -174,16 +168,13 @@ pipelines: - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) - ret_val=$? - - if [ $ret_val -eq 2 ]; - - then - - if [ "$violations" != "[]" ] ; - - then - - echo "$violations" - - echo "Violations found, not proceeding with terraform apply" - - exit 1 - - fi - - elif [ $ret_val -ne 0 ]; - - then + - if [ $ret_val -eq 2 ]; then + - if [ "$violations" != "[]" ] ; then + - echo "$violations" + - echo "Violations found, not proceeding with terraform apply" + - exit 1 + - fi + - elif [ $ret_val -ne 0 ]; then - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" - exit 1 - else From 23900c85bcffdf43bc1888f14388d752e83bfe58 Mon Sep 17 00:00:00 2001 From: Shubham Singh Date: Sat, 25 Mar 2023 02:26:20 +0530 Subject: [PATCH 128/132] adjust coding style --- .../bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml | 2 +- .../project-factory/.bitbucket/bitbucket-pipelines.yml | 2 +- .../bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml index fc1b06b..eb26803 100644 --- a/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml +++ b/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml @@ -62,7 +62,7 @@ pipelines: - /usr/bin/terraform plan -out $TF_PLAN_NAME - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) - - ret_val=$? + - ret_val=$? - if [ $ret_val -eq 2 ]; then - if [ "$violations" != "[]" ] ; then - echo "$violations" diff --git a/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml index 020e2b7..562d91b 100644 --- a/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml +++ b/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml @@ -62,7 +62,7 @@ pipelines: - /usr/bin/terraform plan -out $TF_PLAN_NAME - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) - - ret_val=$? + - ret_val=$? - if [ $ret_val -eq 2 ]; then - if [ "$violations" != "[]" ] ; then - echo "$violations" diff --git a/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml index 7c6c1de..b2969ac 100644 --- a/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml +++ b/examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml @@ -167,7 +167,7 @@ pipelines: - /usr/bin/terraform plan -out $TF_PLAN_NAME - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) - - ret_val=$? + - ret_val=$? - if [ $ret_val -eq 2 ]; then - if [ "$violations" != "[]" ] ; then - echo "$violations" From 84f620bb223f8745c597026c4994660675d64a75 Mon Sep 17 00:00:00 2001 From: Saborni Das Date: Wed, 29 Mar 2023 17:24:26 +0530 Subject: [PATCH 129/132] Updated jenkinsfile and readme file for folder factory --- .../jenkins/folder-factory/Jenkinsfile | 51 +++++++++++-------- .../jenkins/folder-factory/README.md | 45 +++++++++++++++- 2 files changed, 74 insertions(+), 22 deletions(-) diff --git a/examples/guardrails/jenkins/folder-factory/Jenkinsfile b/examples/guardrails/jenkins/folder-factory/Jenkinsfile index 63cca86..f69e778 100644 --- a/examples/guardrails/jenkins/folder-factory/Jenkinsfile +++ b/examples/guardrails/jenkins/folder-factory/Jenkinsfile @@ -1,15 +1,14 @@ -pipeline { - +pipeline { agent any environment { - BUCKET_PATH = credentials('backend-path') - REPO_FULL_NAME = credentials('bucket-repo') - PROJECT_NAME = credentials('project-name') - GCP_PROJECT_NUMBER = credentials('project-id') - workload_identity_pool_id = credentials('wif_pool_id') - workload_identity_pool_provider_id = credentials('wif_pool_provider_id') - SERVICE_ACCOUNT_NAME = credentials('sa-name') - + BUCKET_PATH = credentials('backend-path') + REPO_FULL_NAME = credentials('bucket-repo') + PROJECT_NAME = credentials('project-name') + GCP_PROJECT_NUMBER = credentials('project-id') + workload_identity_pool_id = credentials('wif_pool_id') + workload_identity_pool_provider_id = credentials('wif_pool_provider_id') + SERVICE_ACCOUNT_NAME = credentials('sa-name') + policy_file_path = credentials('policy_file_path') } options { skipDefaultCheckout(true) @@ -20,7 +19,7 @@ pipeline { cleanWs() } } - stage('WIF') { + stage('WIF') { steps { withCredentials([file(variable: 'ID_TOKEN_FILE', credentialsId: 'gcp')]) { writeFile file: "$WORKSPACE_TMP/creds.json", text: """ @@ -41,8 +40,7 @@ pipeline { sh ''' gcloud auth login --brief --cred-file=$WORKSPACE_TMP/creds.json ''' -} - +} } } stage('checkout') { @@ -50,21 +48,34 @@ pipeline { checkout scm } } - stage('Terraform') { + stage('Terraform Plan') { steps { sh ''' cd guardrails/folder-factory terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${REPO_FULL_NAME}" - terraform plan + terraform plan -input=false -out ffjenkins.tfplan + ''' + } + } + stage('Terraform Validate') { + steps { + sh ''' + cd guardrails/folder-factory + terraform show -json "ffjenkins.tfplan" > "ffjenkins.json" + gcloud source repos clone gcp-policies "${policy_file_path}" --project="${GCP_PROJECT_NUMBER}" + gcloud beta terraform vet "ffjenkins.json" --policy-library="${policy_file_path}" --project="${GCP_PROJECT_NUMBER}" + ''' + } + } + stage('Terraform Apply') { + steps { + sh ''' + cd guardrails/folder-factory terraform apply -auto-approve - - ''' - + ''' } } - - } } diff --git a/examples/guardrails/jenkins/folder-factory/README.md b/examples/guardrails/jenkins/folder-factory/README.md index 05b739c..43dd0b0 100644 --- a/examples/guardrails/jenkins/folder-factory/README.md +++ b/examples/guardrails/jenkins/folder-factory/README.md @@ -4,7 +4,7 @@ This is a template for a DevOps folder factory. It can be used with [https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory](https://github.com/google/devops-governance/tree/main/examples/guardrails/project-factory) and is intended to house the folder configurations: -Screenshot 2023-03-10 at 03 10 19 +![Screenshot 2022-05-10 12 00 19 PM](https://user-images.githubusercontent.com/94000358/169809437-aaa8538e-3ffc-48b3-9028-84e4995de150.png) Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. @@ -56,4 +56,45 @@ iam: - serviceAccount:XXXXX@XXXXXX ``` -Every folder is defined with its own yaml file located in the following [Folder](data/folders). +Every folder is defined with its own yaml file located in the following [Folder](data/folder). + +## How to run this stage +### Prerequisites + +Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. There is a sample code provided in “folder.yaml.sample” to create a folder and for terraform to create a folder minimum below permissions are required. +“Folder Creator” or “Folder Admin” at org level +“Organization Policy Admin” at org level + + +### Installation Steps + +Step 1: Create a bucket for terraform backend on the GCP environment. +Step 2: Create Jenkins Pipeline on Jenkins. +Step 3: Configure the below variables on Jenkins Credentials. + +### Terraform config validator +The pipeline has an option to utilise the integrated config validator (gcloud terraform vet) to impose constraints on your terraform configuration. You have to provide the policy-library repo URL to $POLICY_LIBRARY_REPO variable. See the below for details on the Variables to be set on the CI/CD pipeline. + + +| Variable | | Example Value | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | +| PROJECT_NAME | The project containing the service account that has permission to communicate with the WIF provider. Should be created as part of Project Factory. | jenkins-connect-prj | +| GCP_PROJECT_NUMBER | Project number for the project that hosts the WIF provider | 107999111999 | +| SERVICE_ACCOUNT_NAME | The name of the service account that will be used to deploy. Must be hosted in PROJECT_NAME. | jenkins-sa | +| BUCKET_PATH | A state bucket that will hold the terraform state. This bucket must previously exist and the service account must have permission to read/write to it. | jenkins-gcs-state-bucket-name | +| policy_file_path | https://github.com/GoogleCloudPlatform/policy-library | The public repo where the policies are hosted | +| workload_identity_pool_id | | jenkins-test-pool | +| workload_identity_provider_id | | jenkins-test-provider | | + +* Once the prerequisites are set up, any commit to the remote main branch with changes to *.tf, *.tfvars, data/*, modules/* files should trigger the pipeline. + + +### Pipeline Workflow Overview +The complete workflow comprises of 6 stages and 2 before-script jobs + * Stages: + * Clean workspace : This step cleans the previous packages from Jenkins workspace + * WIF : Execute the Workload Identity Federation script and generate credential file. + * Terraform plan: Runs terraform plan and saves the plan and json version of the plan as artifacts, this depends on the branch + * Terraform validate: Runs gcloud terraform vet against the terraform code with the constraints in the specified repository. + * apply: This is executed for specified list of branches, currently main/master + From 402cd054a0e958dab0f11917d54ecc7c07de011f Mon Sep 17 00:00:00 2001 From: Saborni Das Date: Wed, 29 Mar 2023 17:28:46 +0530 Subject: [PATCH 130/132] Updated Jenkinsfile and readme files --- .../jenkins/project-factory/Jenkinsfile | 38 ++++++++------ .../jenkins/project-factory/README.md | 44 ++++++++++++++++- .../guardrails/jenkins/skunkworks/Jenkinsfile | 49 ++++++++++++------- .../guardrails/jenkins/skunkworks/README.md | 42 +++++++++++++++- 4 files changed, 139 insertions(+), 34 deletions(-) diff --git a/examples/guardrails/jenkins/project-factory/Jenkinsfile b/examples/guardrails/jenkins/project-factory/Jenkinsfile index a7f9447..5543a60 100644 --- a/examples/guardrails/jenkins/project-factory/Jenkinsfile +++ b/examples/guardrails/jenkins/project-factory/Jenkinsfile @@ -1,12 +1,9 @@ pipeline { - agent any environment { - BUCKET_PATH = credentials('backend-path') - - + BUCKET_PATH = credentials('backend-path') + policy_file_path = credentials('policy_file_path') } - options { skipDefaultCheckout(true) } @@ -21,18 +18,31 @@ pipeline { checkout scm } } - - stage('Terraform') { - steps { - + stage('Terraform Plan') { + steps { sh ''' cd guardrails/project-factory terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=project-factory-staging-tfstate" -var-file="staging.tfvars" - terraform plan -var-file="staging.tfvars" - terraform apply -var-file="staging.tfvars" -auto-approve - - ''' - + terraform plan -var-file="staging.tfvars" -out pfjenkins.tfplan + ''' + } + } + stage('Terraform Validate') { + steps { + sh ''' + cd guardrails/project-factory + terraform show -json "pfjenkins.tfplan" > "ffjenkins.json" + gcloud source repos clone gcp-policies "${policy_file_path}" --project="${GCP_PROJECT_NUMBER}" + gcloud beta terraform vet "pfjenkins.json" --policy-library="${policy_file_path}" --project="${GCP_PROJECT_NUMBER}" + ''' + } + } + stage('Terraform Apply') { + steps { + sh ''' + cd guardrails/project-factory + terraform apply -var-file="staging.tfvars" -auto-approve + ''' } } } diff --git a/examples/guardrails/jenkins/project-factory/README.md b/examples/guardrails/jenkins/project-factory/README.md index 4fe5630..bd5b101 100644 --- a/examples/guardrails/jenkins/project-factory/README.md +++ b/examples/guardrails/jenkins/project-factory/README.md @@ -8,7 +8,7 @@ It can be used with https://github.com/google/devops-governance/tree/main/exampl Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. -Screenshot 2023-03-10 at 03 10 46 +![Folder Factory](https://user-images.githubusercontent.com/94000358/169809882-f5ff9fb1-d037-49de-8c2c-bf0d457b662f.png) The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. @@ -73,3 +73,45 @@ repo_branch: dev ``` Every project is defined with its own file located in the [Project Folder](data/projects). + +## How to run this stage +### Prerequisites + +Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. There is a sample code provided in “folder.yaml.sample” to create a folder and for terraform to create a folder minimum below permissions are required. +“Folder Creator” or “Folder Admin” at org level +“Organization Policy Admin” at org level + + +### Installation Steps + +Step 1: Create a bucket for terraform backend on the GCP environment. +Step 2: Create Jenkins Pipeline on Jenkins. +Step 3: Configure the below variables on Jenkins Credentials. + +### Terraform config validator +The pipeline has an option to utilise the integrated config validator (gcloud terraform vet) to impose constraints on your terraform configuration. You have to provide the policy-library repo URL to $POLICY_LIBRARY_REPO variable. See the below for details on the Variables to be set on the CI/CD pipeline. + + +| Variable | | Example Value | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | +| PROJECT_NAME | The project containing the service account that has permission to communicate with the WIF provider. Should be created as part of Project Factory. | jenkins-connect-prj | +| GCP_PROJECT_NUMBER | Project number for the project that hosts the WIF provider | 107999111999 | +| SERVICE_ACCOUNT_NAME | The name of the service account that will be used to deploy. Must be hosted in PROJECT_NAME. | jenkins-sa | +| BUCKET_PATH | A state bucket that will hold the terraform state. This bucket must previously exist and the service account must have permission to read/write to it. | jenkins-gcs-state-bucket-name | +| policy_file_path | https://github.com/GoogleCloudPlatform/policy-library | The public repo where the policies are hosted | +| workload_identity_pool_id | | jenkins-test-pool | +| workload_identity_provider_id | | jenkins-test-provider | | + +* Once the prerequisites are set up, any commit to the remote main branch with changes to *.tf, *.tfvars, data/*, modules/* files should trigger the pipeline. + + +### Pipeline Workflow Overview +The complete workflow comprises of 6 stages and 2 before-script jobs + * Stages: + * Clean workspace : This step cleans the previous packages from Jenkins workspace + * WIF : Execute the Workload Identity Federation script and generate credential file. + * Terraform plan: Runs terraform plan and saves the plan and json version of the plan as artifacts, this depends on the branch + * Terraform validate: Runs gcloud terraform vet against the terraform code with the constraints in the specified repository. + * apply: This is executed for specified list of branches, currently main/master + + diff --git a/examples/guardrails/jenkins/skunkworks/Jenkinsfile b/examples/guardrails/jenkins/skunkworks/Jenkinsfile index 099eed3..e2856a0 100644 --- a/examples/guardrails/jenkins/skunkworks/Jenkinsfile +++ b/examples/guardrails/jenkins/skunkworks/Jenkinsfile @@ -1,11 +1,13 @@ -pipeline { - +pipeline { agent any environment { - BUCKET_PATH = credentials('backend-path') - SKUNKWORK_PROJECT = credentials('staging-project') - SW_PROJECT_NUMBER = credentials('sw-project-number') - SW_SERVICE_ACCOUNT = credentials('sw-sa') + BUCKET_PATH = credentials('backend-path') + SKUNKWORK_PROJECT = credentials('staging-project') + SW_PROJECT_NUMBER = credentials('sw-project-number') + SW_SERVICE_ACCOUNT = credentials('sw-sa') + SW_workload_identity_pool_id = credentials('sw_wif_pool_id') + SW_workload_identity_pool_provider_id = credentials('sw_wif_pool_provider_id') + policy_file_path = credentials('policy_file_path') } options { skipDefaultCheckout(true) @@ -22,7 +24,7 @@ pipeline { writeFile file: "$WORKSPACE_TMP/sts_creds.json", text: """ { "type": "external_account", - "audience": "//iam.googleapis.com/projects/${SW_PROJECT_NUMBER}/locations/global/workloadIdentityPools/jenkins-pool1-ad8aa16a/providers/jenkins-provider-ad8aa16a", + "audience": "//iam.googleapis.com/projects/${SW_PROJECT_NUMBER}/locations/global/workloadIdentityPools/${SW_workload_identity_pool_id}/providers/${SW_workload_identity_pool_provider_id}", "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", "token_url": "https://sts.googleapis.com/v1/token", "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SW_SERVICE_ACCOUNT}@${SKUNKWORK_PROJECT}.iam.gserviceaccount.com:generateAccessToken", @@ -40,26 +42,37 @@ pipeline { } } } - stage('checkout') { steps { checkout scm } } - stage('Terraform') { - steps { - + stage('Terraform Plan') { + steps { sh ''' cd guardrails/skunkworks terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=prod-skunkworks" - terraform plan -var="project=${SKUNKWORK_PROJECT}" - terraform apply -var="project=${SKUNKWORK_PROJECT}" -auto-approve - - ''' - + terraform plan -var="project=${SKUNKWORK_PROJECT}" -out swjenkins.tfplan + ''' + } + } + stage('Terraform Validate') { + steps { + sh ''' + cd guardrails/skunkworks + terraform show -json "swjenkins.tfplan" > "swjenkins.json" + gcloud source repos clone gcp-policies "${policy_file_path}" --project="${SW_PROJECT_NUMBER}" + gcloud beta terraform vet "swjenkins.json" --policy-library="${policy_file_path}" --project="${SW_PROJECT_NUMBER}" + ''' } } - - + stage('Terraform Apply') { + steps { + sh ''' + cd guardrails/skunkworks + terraform apply -var="project=${SKUNKWORK_PROJECT}" -auto-approve + ''' + } + } } } diff --git a/examples/guardrails/jenkins/skunkworks/README.md b/examples/guardrails/jenkins/skunkworks/README.md index 0304c23..7602c43 100644 --- a/examples/guardrails/jenkins/skunkworks/README.md +++ b/examples/guardrails/jenkins/skunkworks/README.md @@ -2,7 +2,7 @@ This is a template for an IaC kickstarter repository. -Screenshot 2023-03-10 at 03 11 13 +![Skunkworks](https://user-images.githubusercontent.com/94000358/169810982-36f01de2-e5e5-4ecd-b98e-3cf5a6aa9f81.png) The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. @@ -27,3 +27,43 @@ env: SERVICE_ACCOUNT: 'XXXX@XXXX' # The service account that should be used for this repository. ``` +## How to run this stage +### Prerequisites + +Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. There is a sample code provided in “folder.yaml.sample” to create a folder and for terraform to create a folder minimum below permissions are required. +“Folder Creator” or “Folder Admin” at org level +“Organization Policy Admin” at org level + + +### Installation Steps + +Step 1: Create a bucket for terraform backend on the GCP environment. +Step 2: Create Jenkins Pipeline on Jenkins. +Step 3: Configure the below variables on Jenkins Credentials. + +### Terraform config validator +The pipeline has an option to utilise the integrated config validator (gcloud terraform vet) to impose constraints on your terraform configuration. You have to provide the policy-library repo URL to $POLICY_LIBRARY_REPO variable. See the below for details on the Variables to be set on the CI/CD pipeline. + + +| Variable | | Example Value | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | +| PROJECT_NAME | The project containing the service account that has permission to communicate with the WIF provider. Should be created as part of Project Factory. | jenkins-connect-prj | +| GCP_PROJECT_NUMBER | Project number for the project that hosts the WIF provider | 107999111999 | +| SERVICE_ACCOUNT_NAME | The name of the service account that will be used to deploy. Must be hosted in PROJECT_NAME. | jenkins-sa | +| BUCKET_PATH | A state bucket that will hold the terraform state. This bucket must previously exist and the service account must have permission to read/write to it. | jenkins-gcs-state-bucket-name | +| policy_file_path | https://github.com/GoogleCloudPlatform/policy-library | The public repo where the policies are hosted | +| workload_identity_pool_id | | jenkins-test-pool | +| workload_identity_provider_id | | jenkins-test-provider | | + +* Once the prerequisites are set up, any commit to the remote main branch with changes to *.tf, *.tfvars, data/*, modules/* files should trigger the pipeline. + + +### Pipeline Workflow Overview +The complete workflow comprises of 6 stages and 2 before-script jobs + * Stages: + * Clean workspace : This step cleans the previous packages from Jenkins workspace + * WIF : Execute the Workload Identity Federation script and generate credential file. + * Terraform plan: Runs terraform plan and saves the plan and json version of the plan as artifacts, this depends on the branch + * Terraform validate: Runs gcloud terraform vet against the terraform code with the constraints in the specified repository. + * apply: This is executed for specified list of branches, currently main/master + From 2d9288a031de28f80cc90325dc69b16c74f0efef Mon Sep 17 00:00:00 2001 From: arod0719 Date: Wed, 29 Mar 2023 15:36:06 +0000 Subject: [PATCH 131/132] changes --- examples/guardrails/bitbucket/README.md | 32 +-- .../.bitbucket/bitbucket-pipelines.yml | 92 -------- .../bitbucket/folder-factory/README.md | 122 ++++------- .../data/folders/bitbucket-folder.yaml | 15 ++ .../bitbucket/folder-factory/get_oidctoken.sh | 7 - .../bitbucket/folder-factory/provider.tf | 2 + .../bitbucket/project-factory/README.md | 56 +---- .../data/projects/bitbucket-project.yaml | 18 ++ .../project-factory/get_oidctoken.sh | 7 - .../bitbucket/project-factory/main.tf | 2 +- .../bitbucket/project-factory/provider.tf | 2 + .../project-factory/terraform.tfvars | 7 + .../bitbucket/project-factory/variables.tf | 2 +- .../bitbucket/project-factory/wif.tf | 2 +- .../.bitbucket/bitbucket-pipelines.yml | 198 ------------------ .../guardrails/bitbucket/skunkworks/README.md | 95 ++------- .../bitbucket-pipelines.yml | 74 +++++-- .../bitbucket/skunkworks/get_oidctoken.sh | 11 +- .../bitbucket/skunkworks/terraform.tfvars | 1 + 19 files changed, 189 insertions(+), 556 deletions(-) delete mode 100644 examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml create mode 100644 examples/guardrails/bitbucket/folder-factory/data/folders/bitbucket-folder.yaml delete mode 100755 examples/guardrails/bitbucket/folder-factory/get_oidctoken.sh create mode 100644 examples/guardrails/bitbucket/project-factory/data/projects/bitbucket-project.yaml delete mode 100755 examples/guardrails/bitbucket/project-factory/get_oidctoken.sh create mode 100644 examples/guardrails/bitbucket/project-factory/terraform.tfvars delete mode 100644 examples/guardrails/bitbucket/skunkworks/.bitbucket/bitbucket-pipelines.yml rename examples/guardrails/bitbucket/{project-factory/.bitbucket => skunkworks}/bitbucket-pipelines.yml (51%) create mode 100644 examples/guardrails/bitbucket/skunkworks/terraform.tfvars diff --git a/examples/guardrails/bitbucket/README.md b/examples/guardrails/bitbucket/README.md index 0df1adc..696677c 100644 --- a/examples/guardrails/bitbucket/README.md +++ b/examples/guardrails/bitbucket/README.md @@ -20,18 +20,24 @@ A video tutorial covering how to set up the guardrails for Github can be found h # Getting started -## Workload Identity federation -Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. -This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. - -If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw +## Overview +This Runbook contains three repositories; project-factory, folder-factory, and skunkworks. The first two of these repositories do not need to be deployed inside of a traditional bitbucket pipeline. They will deploy the necessary components to establish a structure within GCP and set up a Workload Identity Federation (WIF) provider. The third repository, skunkworks, has an example bitbucket pipeline that will authenticate via the established WIF provider. ### High Level Process -* GCP - - Create a Workload Identity Pool - - Create a Workload Identity Provider - - Create a Service Account and grant permissions - -* CICD tool - - Specify where the pipeline configuration file resides - - Configure variables to pass relevant information to GCP to genrate short-lived tokens + - **Deploy Folder Factory** + - Enter the Folder Factory directory + - Edit provider.tf to contain a backend. Using gcs is suggested by referencing an existing GCS bucket. Use the prefix variable to ensure folder-factory's state exist in a directory within the bucket. + - Add folder yaml files to /data/folders. + - Deploy via Terraform + - **Deploy Project Factory** + - Enter the Project Factory directory + - Edit provider.tf to contain a backend. Using gcs is suggested by referencing an existing GCS bucket. Use the prefix variable to ensure project-factory's state exist in a directory within the bucket. + - Within terraform.tfvars add the proper variables for folder (created with Folder Factory), billing account, Bitbucker Workspace, and allowed Audiences. If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw + - Add project yaml files to /data/projects. + - Deploy via Terraform + - **Deploy Skunkworks** + - Enter the Skunkworks directory + - Edit provider.tf to contain a backend. Using gcs is suggested by referencing an existing GCS bucket. Use the prefix variable to ensure skunkworks' state exist in a directory within the bucket. + - Within terraform.tfvars add the proper variables for the project created in Project Factory + - Within the Bitbucket repository variables, add all the variables described within the Skunkworks README.md. + - Deploy Skunkworks via Bitbucket Pipeline. The pipeline will authenticate to GCP using the Workload Identity Pool created within Project Factory diff --git a/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml deleted file mode 100644 index eb26803..0000000 --- a/examples/guardrails/bitbucket/folder-factory/.bitbucket/bitbucket-pipelines.yml +++ /dev/null @@ -1,92 +0,0 @@ -image: google/cloud-sdk -pipelines: - default: - - step: - name: setup terraform - script: - - apt-get update && apt-get install unzip wget -y - - wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip || echo fine... - - unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip - - yes | cp terraform /usr/bin/ - - ls /usr/bin - artifacts: - - terraform - - step: - oidc: true - name: configure oidc - script: - - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" - - ./get_oidctoken.sh - artifacts: - - sts-creds.json - - /tmp/gcp_access_token.out - - step: - name: terraform init - oidc: true - script: - - cp terraform /usr/bin/ - - ls . /usr/bin/ - - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" - - ./get_oidctoken.sh - - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json - - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" - - /usr/bin/terraform plan - branches: - master: - - step: - name: setup terraform - script: - - apt-get update && apt-get install unzip wget -y - - wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip || echo fine... - - unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip - - yes | cp terraform /usr/bin/ - - ls /usr/bin - artifacts: - - terraform - - step: - name: tf-policy validate - oidc: true - script: - - if [[ "$TERRAFORM_POLICY_VALIDATE" == "true" ]]; then - - cp terraform /usr/bin/ - - export TF_PLAN_NAME=plan.out - - export TF_ROOT=tf_root - - mkdir -p $TF_ROOT - - ls . /usr/bin/ - - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" - - ./get_oidctoken.sh - - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json - - apt-get install google-cloud-sdk-terraform-tools -y - - git clone $POLICY_LIBRARY_REPO $TF_ROOT/policy-repo - - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" - - /usr/bin/terraform plan -out $TF_PLAN_NAME - - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json - - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) - - ret_val=$? - - if [ $ret_val -eq 2 ]; then - - if [ "$violations" != "[]" ] ; then - - echo "$violations" - - echo "Violations found, not proceeding with terraform apply" - - exit 1 - - fi - - elif [ $ret_val -ne 0 ]; then - - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" - - exit 1 - - else - - echo "No policy violations detected; proceeding with terraform apply" - - fi - - fi - - step: - name: master branch execution tf-apply - oidc: true - script: - - cp terraform /usr/bin/ - - ls . /usr/bin/ - - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" - - ./get_oidctoken.sh - - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json - - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" - - /usr/bin/terraform plan - - /usr/bin/terraform apply -auto-approve - - diff --git a/examples/guardrails/bitbucket/folder-factory/README.md b/examples/guardrails/bitbucket/folder-factory/README.md index d842184..8c4b00b 100644 --- a/examples/guardrails/bitbucket/folder-factory/README.md +++ b/examples/guardrails/bitbucket/folder-factory/README.md @@ -1,110 +1,62 @@ # Folder Factory +-------------- -This is a template for a DevOps folder factory. +folder-factory will deploy the folders and establish the organizational hierarchy. Inside the repo is a subdirectory of data/folders that contain yaml files. Each yaml file contains a new folder definition. -It can be used with [https://github.com/google/devops-governance/tree/main/examples/guardrails/bitbucket/project-factory](https://github.com/google/devops-governance/tree/main/examples/guardrails/bitbucket/project-factory) and is intended to house the folder configurations: +## Deployment: -Screenshot 2023-03-10 at 03 11 31 +#### Authentication -Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. +folder-factory should be deployed outside of the normal pipeline process. Workload Identity Federation, which will authorize bitbucket pipelines to communicate with GCP, will not be configured until after the project-factory is deployed. -The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. +Since this is being deployed outside of the pipeline environment, authentication will need to be established separately. If running locally, this can be done with: -## Repository Configuration -This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. +``` +gcloud auth login +``` + +Alternatively, CloudShell can be used for an easy environment that is already GCP authenticated. -If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw +The authenticated user should have permissions in GCP IAM to create folders. If desired, a Service Account may be used that has the required permissions. See [Impersonating Service Accounts](https://cloud.google.com/iam/docs/impersonating-service-accounts) for more information. -After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: +#### Terraform -Secret configuration +After authentication, the terraform can be deployed. Navigate to the root of the folder-factory repository and initialize the terraform. -or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: ``` -env: - STATE_BUCKET: 'XXXX' - # The GCS bucket to store the terraform state - WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' - # The workload identity provider that should be used for this repository. - SERVICE_ACCOUNT: 'XXXX@XXXX' - # The service account that should be used for this repository. +cd folder-factory ``` -## Setting up folders +Before Terraform can be initialized, Terraform needs to be directed on where to store the state file within Google Cloud Storage. Open the file named providers.tf and populate values for bucket and prefix. The modified file should resemble the following: -The folder factory will: -- create a folders with defined organisational policies - -It uses YAML configuration files for every folder with the following sample structure: ``` -parent: folders/XXXXXXXXX -org_policies: - policy_boolean: - constraints/compute.disableGuestAttributesAccess: true - constraints/iam.disableServiceAccountCreation: false - constraints/iam.disableServiceAccountKeyCreation: false - constraints/iam.disableServiceAccountKeyUpload: false - constraints/gcp.disableCloudLogging: false - policy_list: - constraints/compute.vmExternalIpAccess: - inherit_from_parent: null - status: true - suggested_value: null - values: -iam: - roles/resourcemanager.projectCreator: - - serviceAccount:XXXXX@XXXXXX +terraform { +  backend "gcs" { +    bucket = "your-bucket-name" +    prefix = "path/to/state/file/" +  } +} +#  ... ``` -Every folder is defined with its own yaml file located in the following [Folder](data/folders). - - -## How to run this stage -### Prerequisites - -Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. There is a sample code provided in “folder.yaml.sample” to create a folder and for terraform to create a folder minimum below permissions are required. -“Folder Creator” or “Folder Admin” at org level -“Organization Policy Admin” at org level - - -### Installation Steps -From the folder-factory Gitlab project page -* CICD configuration file path - Navigate to Pipelines > Run Pipeline > select branch > select pipeline - Update “CI/CD configuration file” to point to the root of repository, `bitbucket-pipelines.yml` - -* CI/CD variables - Navigate to Deployments > Configure > Add Variables - Add the variables to the pipeline as described in the table below. - -### Terraform config validator -The pipeline has an option to utilise the integrated config validator (gcloud terraform vet) to impose constraints on your terraform configuration. You can enable it by setting the CI/CD Variable $TERRAFORM_POLICY_VALIDATE to "true" and providing the policy-library repo URL to $POLICY_LIBRARY_REPO variable. See the below for details on the Variables to be set on the CI/CD pipeline. +After this change is made, Terraform can be initialized. +``` +terraform init +``` -| Variable | | Example Value | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | -| TERRAFORM_VERSION | Version of Terraform to execute. Terraform will be installed at the beginning of every job. | 1.3.7 | -| PROJECT_NAME | The project containing the service account that has permission to communicate with the WIF provider. Should be created as part of Project Factory. | bitbucket-connect-prj | -| GCP_PROJECT_NUMBER | Project number for the project that hosts the WIF provider | 107999111999 | -| SERVICE_ACCOUNT_NAME | The name of the service account that will be used to deploy. Must be hosted in PROJECT_NAME. | bb-service-acct | -| BUCKET_PATH | A state bucket that will hold the terraform state. This bucket must previously exist and the service account must have permission to read/write to it. | gcs-state-bucket-name | -| POLICY_LIBRARY_REPO | https://github.com/GoogleCloudPlatform/policy-library | The public repo where the policies are hosted | -| TERRAFORM_POLICY_VALIDATE | This is a flag that should be set to “true” in order to enable the policy validation | Should be set to “true” to enable validation | -| workload_identity_pool_id | | bitbucket-test-pool | -| workload_identity_provider_id | | bitbucket-test-provider | | +This is when to add, change, or modify any of the yaml files in the data/folders directory. See sample yaml provided for details. Each yaml file contains the definition of a new GCP folder. -* Once the prerequisites are set up, any commit to the remote main branch with changes to *.tf, *.tfvars, data/*, modules/* files should trigger the pipeline. +Upon making any modifications, run the following to plan the deployment. +``` +terraform plan +``` -* get_oidctoken.sh should run successfully in the pipeline if the workload identity federation is configured as required. +After reviewing the proposed infrastructure changes, approve the deployment. -### Pipeline Workflow Overview -The complete workflow comprises of 4-5 stages and 2 before-script jobs - * setup terraform : - * This step should download the terraform from internet and keep that as an artifacts to be used in later stages - * Stages: - * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages - * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts, this depends on the branch - * policy-validate: Runs gcloud terraform vet against the terraform code with the constraints in the specified repository. - * apply: This is executed for specified list of branches, currently main/master +``` +terraform apply -auto-approve +``` +Folders should now be deployed into your GCP environment \ No newline at end of file diff --git a/examples/guardrails/bitbucket/folder-factory/data/folders/bitbucket-folder.yaml b/examples/guardrails/bitbucket/folder-factory/data/folders/bitbucket-folder.yaml new file mode 100644 index 0000000..84485a2 --- /dev/null +++ b/examples/guardrails/bitbucket/folder-factory/data/folders/bitbucket-folder.yaml @@ -0,0 +1,15 @@ +# 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 +# +# 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. + +parent: #Example: organizations/123456789 diff --git a/examples/guardrails/bitbucket/folder-factory/get_oidctoken.sh b/examples/guardrails/bitbucket/folder-factory/get_oidctoken.sh deleted file mode 100755 index b032b70..0000000 --- a/examples/guardrails/bitbucket/folder-factory/get_oidctoken.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -echo -n "${BITBUCKET_STEP_OIDC_TOKEN}" > /tmp/gcp_access_token.out -gcloud iam workload-identity-pools create-cred-config "projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/${workload_identity_pool_id}/providers/${workload_identity_pool_provider_id}" --credential-source-file=/tmp/gcp_access_token.out --service-account="${SERVICE_ACCOUNT_EMAIL}" --output-file=sts-creds.json -export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json -gcloud auth login --cred-file=`pwd`/sts-creds.json -gcloud config set project $PROJECT_NAME -gcloud services list \ No newline at end of file diff --git a/examples/guardrails/bitbucket/folder-factory/provider.tf b/examples/guardrails/bitbucket/folder-factory/provider.tf index 0e17ef9..73f8561 100644 --- a/examples/guardrails/bitbucket/folder-factory/provider.tf +++ b/examples/guardrails/bitbucket/folder-factory/provider.tf @@ -16,6 +16,8 @@ terraform { backend "gcs" { + bucket = "" + prefix = "" #ex: bucket/folder-factory } } diff --git a/examples/guardrails/bitbucket/project-factory/README.md b/examples/guardrails/bitbucket/project-factory/README.md index f40ee7b..f901e7f 100644 --- a/examples/guardrails/bitbucket/project-factory/README.md +++ b/examples/guardrails/bitbucket/project-factory/README.md @@ -2,13 +2,13 @@ This is a template for a DevOps project factory. -It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/bitbucket/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/bitbucket/folder-factory) and is intended to house the projects of a specified folder: +It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory) and is intended to house the projects of a specified folder: Overview Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. -Screenshot 2023-03-10 at 03 11 54 +![Folder Factory](https://user-images.githubusercontent.com/94000358/169809882-f5ff9fb1-d037-49de-8c2c-bf0d457b662f.png) The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. @@ -73,55 +73,3 @@ repo_branch: dev ``` Every project is defined with its own file located in the [Project Folder](data/projects). - - - -## How to run this stage -### Prerequisites - -Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. There is a sample code provided in “folder.yaml.sample” to create a folder and for terraform to create a folder minimum below permissions are required. -“Folder Creator” or “Folder Admin” at org level -“Organization Policy Admin” at org level - - -### Installation Steps -From the folder-factory Gitlab project page -* CICD configuration file path - Navigate to Pipelines > Run Pipeline > select branch > select pipeline - Update “CI/CD configuration file” to point to the root of repository, `bitbucket-pipelines.yml` - -* CI/CD variables - Navigate to Deployments > Configure > Add Variables - Add the variables to the pipeline as described in the table below. - -### Terraform config validator -The pipeline has an option to utilise the integrated config validator (gcloud terraform vet) to impose constraints on your terraform configuration. You can enable it by setting the CI/CD Variable $TERRAFORM_POLICY_VALIDATE to "true" and providing the policy-library repo URL to $POLICY_LIBRARY_REPO variable. See the below for details on the Variables to be set on the CI/CD pipeline. - - -| Variable | | Example Value | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | -| TERRAFORM_VERSION | Version of Terraform to execute. Terraform will be installed at the beginning of every job. | 1.3.7 | -| PROJECT_NAME | The project containing the service account that has permission to communicate with the WIF provider. Should be created as part of Project Factory. | bitbucket-connect-prj | -| GCP_PROJECT_NUMBER | Project number for the project that hosts the WIF provider | 107999111999 | -| SERVICE_ACCOUNT_NAME | The name of the service account that will be used to deploy. Must be hosted in PROJECT_NAME. | bb-service-acct | -| BUCKET_PATH | A state bucket that will hold the terraform state. This bucket must previously exist and the service account must have permission to read/write to it. | gcs-state-bucket-name | -| POLICY_LIBRARY_REPO | https://github.com/GoogleCloudPlatform/policy-library | The public repo where the policies are hosted | -| TERRAFORM_POLICY_VALIDATE | This is a flag that should be set to “true” in order to enable the policy validation | Should be set to “true” to enable validation | -| workload_identity_pool_id | | bitbucket-test-pool | -| workload_identity_provider_id | | bitbucket-test-provider | | - -* Once the prerequisites are set up, any commit to the remote main branch with changes to *.tf, *.tfvars, data/*, modules/* files should trigger the pipeline. - - -* get_oidctoken.sh should run successfully in the pipeline if the workload identity federation is configured as required. - -### Pipeline Workflow Overview -The complete workflow comprises of 4-5 stages and 2 before-script jobs - * setup terraform : - * This step should download the terraform from internet and keep that as an artifacts to be used in later stages - * Stages: - * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages - * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts, this depends on the branch - * policy-validate: Runs gcloud terraform vet against the terraform code with the constraints in the specified repository. - * apply: This is executed for specified list of branches, currently main/master - diff --git a/examples/guardrails/bitbucket/project-factory/data/projects/bitbucket-project.yaml b/examples/guardrails/bitbucket/project-factory/data/projects/bitbucket-project.yaml new file mode 100644 index 0000000..ad10434 --- /dev/null +++ b/examples/guardrails/bitbucket/project-factory/data/projects/bitbucket-project.yaml @@ -0,0 +1,18 @@ +# 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 +# +# 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. + +billing_account_id: +repo_provider: Bitbucket +repo_branch: Production +folder: folders/123456789 \ No newline at end of file diff --git a/examples/guardrails/bitbucket/project-factory/get_oidctoken.sh b/examples/guardrails/bitbucket/project-factory/get_oidctoken.sh deleted file mode 100755 index b032b70..0000000 --- a/examples/guardrails/bitbucket/project-factory/get_oidctoken.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -echo -n "${BITBUCKET_STEP_OIDC_TOKEN}" > /tmp/gcp_access_token.out -gcloud iam workload-identity-pools create-cred-config "projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/${workload_identity_pool_id}/providers/${workload_identity_pool_provider_id}" --credential-source-file=/tmp/gcp_access_token.out --service-account="${SERVICE_ACCOUNT_EMAIL}" --output-file=sts-creds.json -export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json -gcloud auth login --cred-file=`pwd`/sts-creds.json -gcloud config set project $PROJECT_NAME -gcloud services list \ No newline at end of file diff --git a/examples/guardrails/bitbucket/project-factory/main.tf b/examples/guardrails/bitbucket/project-factory/main.tf index 2f0eff9..29d70e0 100644 --- a/examples/guardrails/bitbucket/project-factory/main.tf +++ b/examples/guardrails/bitbucket/project-factory/main.tf @@ -28,7 +28,7 @@ module "project" { repo_sub = each.value.repo_branch repo_provider = each.value.repo_provider billing_account = each.value.billing_account_id - folder = var.folder + folder = each.value.folder roles = try(each.value.roles, []) wif-pool = google_iam_workload_identity_pool.wif-pool-bitbucket.name depends_on = [google_iam_workload_identity_pool.wif-pool-bitbucket] diff --git a/examples/guardrails/bitbucket/project-factory/provider.tf b/examples/guardrails/bitbucket/project-factory/provider.tf index 34beb2f..d37bb09 100644 --- a/examples/guardrails/bitbucket/project-factory/provider.tf +++ b/examples/guardrails/bitbucket/project-factory/provider.tf @@ -16,6 +16,8 @@ terraform { backend "gcs" { + bucket = "" + prefix = "" #ex: bucket/project-factory } } diff --git a/examples/guardrails/bitbucket/project-factory/terraform.tfvars b/examples/guardrails/bitbucket/project-factory/terraform.tfvars new file mode 100644 index 0000000..a0e989e --- /dev/null +++ b/examples/guardrails/bitbucket/project-factory/terraform.tfvars @@ -0,0 +1,7 @@ +folder = "" #Ex: "folders/123456789" + +billing_account = "BillingAccount" #Ex: 123456-123456-123456 + +workspace = "" + +allowed_audiences = [" $TF_ROOT/tfplan.json - - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) - - ret_val=$? - - if [ $ret_val -eq 2 ]; then - - if [ "$violations" != "[]" ] ; then - - echo "$violations" - - echo "Violations found, not proceeding with terraform apply" - - exit 1 - - fi - - elif [ $ret_val -ne 0 ]; then - - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" - - exit 1 - - else - - echo "No policy violations detected; proceeding with terraform apply" - - fi - - fi - - step: - name: master branch execution tf-apply - oidc: true - script: - - cp terraform /usr/bin/ - - ls . /usr/bin/ - - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" - - echo $GCP_PROJECT_NUMBER ${workload_identity_pool_id} ${workload_identity_pool_provider_id} ${SERVICE_ACCOUNT_EMAIL} $PROJECT_NAME - - ./get_oidctoken.sh - - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json - - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" - - /usr/bin/terraform plan - - /usr/bin/terraform apply -auto-approve - dev: - - step: - name: setup terraform - script: - - apt-get update && apt-get install unzip wget -y - - wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip || echo fine... - - unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip - - yes | cp terraform /usr/bin/ - - ls /usr/bin - artifacts: - - terraform - - step: - name: tf-policy validate - oidc: true - script: - - if [[ "$TERRAFORM_POLICY_VALIDATE" == "true" ]]; then - - cp terraform /usr/bin/ - - export TF_PLAN_NAME=plan.out - - export TF_ROOT=tf_root - - mkdir -p $TF_ROOT - - ls . /usr/bin/ - - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" - - ./get_oidctoken.sh - - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json - - apt-get install google-cloud-sdk-terraform-tools -y - - git clone $POLICY_LIBRARY_REPO $TF_ROOT/policy-repo - - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" - - /usr/bin/terraform plan -out $TF_PLAN_NAME - - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json - - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) - - ret_val=$? - - if [ $ret_val -eq 2 ]; then - - if [ "$violations" != "[]" ] ; then - - echo "$violations" - - echo "Violations found, not proceeding with terraform apply" - - exit 1 - - fi - - elif [ $ret_val -ne 0 ]; then - - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" - - exit 1 - - else - - echo "No policy violations detected; proceeding with terraform apply" - - fi - - fi - - step: - name: dev branch execution tf-apply - oidc: true - script: - - cp terraform /usr/bin/ - - ls . /usr/bin/ - - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" - - echo $GCP_PROJECT_NUMBER ${workload_identity_pool_id} ${workload_identity_pool_provider_id} ${SERVICE_ACCOUNT_EMAIL} $PROJECT_NAME - - ./get_oidctoken.sh - - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json - - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" - - /usr/bin/terraform plan - - /usr/bin/terraform apply -auto-approve - prod: - - step: - name: setup terraform - script: - - apt-get update && apt-get install unzip wget -y - - wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip || echo fine... - - unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip - - yes | cp terraform /usr/bin/ - - ls /usr/bin - artifacts: - - terraform - - step: - name: tf-policy validate - oidc: true - script: - - if [[ "$TERRAFORM_POLICY_VALIDATE" == "true" ]]; then - - cp terraform /usr/bin/ - - export TF_PLAN_NAME=plan.out - - export TF_ROOT=tf_root - - mkdir -p $TF_ROOT - - ls . /usr/bin/ - - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" - - ./get_oidctoken.sh - - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json - - apt-get install google-cloud-sdk-terraform-tools -y - - git clone $POLICY_LIBRARY_REPO $TF_ROOT/policy-repo - - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" - - /usr/bin/terraform plan -out $TF_PLAN_NAME - - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json - - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) - - ret_val=$? - - if [ $ret_val -eq 2 ]; then - - if [ "$violations" != "[]" ] ; then - - echo "$violations" - - echo "Violations found, not proceeding with terraform apply" - - exit 1 - - fi - - elif [ $ret_val -ne 0 ]; then - - echo "Error during gcloud beta terraform vet; not proceeding with terraform apply" - - exit 1 - - else - - echo "No policy violations detected; proceeding with terraform apply" - - fi - - fi - - step: - name: prod branch execution tf-apply - oidc: true - script: - - cp terraform /usr/bin/ - - ls . /usr/bin/ - - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" - - echo $GCP_PROJECT_NUMBER ${workload_identity_pool_id} ${workload_identity_pool_provider_id} ${SERVICE_ACCOUNT_EMAIL} $PROJECT_NAME - - ./get_oidctoken.sh - - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json - - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" - - /usr/bin/terraform plan - - /usr/bin/terraform apply -auto-approve - - diff --git a/examples/guardrails/bitbucket/skunkworks/README.md b/examples/guardrails/bitbucket/skunkworks/README.md index c04ff25..b34cc01 100644 --- a/examples/guardrails/bitbucket/skunkworks/README.md +++ b/examples/guardrails/bitbucket/skunkworks/README.md @@ -1,81 +1,30 @@ -# Skunkworks - IaC Kickstarter Template +# Skunkworks - Bitbucket Pipelines Terraform Example with Workload Identity Federation -This is a template for an IaC kickstarter repository. +This is a template for an IaC Terraform repository that includes a Bitbucket pipeline that authenticates via Workload Identity Federation. It is intended to be run after folder-factory and project-factory. -Screenshot 2023-03-10 at 03 12 19 +![Skunkworks](https://user-images.githubusercontent.com/94000358/169810982-36f01de2-e5e5-4ecd-b98e-3cf5a6aa9f81.png) -The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. -This template creates a bucket in the specified target environment. -## Repository Configuration -This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. - -If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw - -After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: - -Secret configuration - -or by modifing the [Workflow Action Files](.github/workflows/) and setting the environment variables: -``` -env: - STATE_BUCKET: 'XXXX' - # The GCS bucket to store the terraform state - WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' - # The workload identity provider that should be used for this repository. - SERVICE_ACCOUNT: 'XXXX@XXXX' - # The service account that should be used for this repository. -``` - - - -## How to run this stage -### Prerequisites - -Workload Identity setup between the folder factory gitlab repositories and the GCP Identity provider configured with a service account containing required permissions to create folders and their organizational policies. There is a sample code provided in “folder.yaml.sample” to create a folder and for terraform to create a folder minimum below permissions are required. -“Folder Creator” or “Folder Admin” at org level -“Organization Policy Admin” at org level +This is simple repository that only deploys a GCS bucket into a specified project. The point of this project is to prove that Terraform can be deployed to Bitbucket via Workload Identity Federation. +## Repository Configuration -### Installation Steps -From the folder-factory Gitlab project page -* CICD configuration file path - Navigate to Pipelines > Run Pipeline > select branch > select pipeline - Update “CI/CD configuration file” to point to the root of repository, `bitbucket-pipelines.yml` - -* CI/CD variables - Navigate to Deployments > Configure > Add Variables - Add the variables to the pipeline as described in the table below. - -### Terraform config validator -The pipeline has an option to utilise the integrated config validator (gcloud terraform vet) to impose constraints on your terraform configuration. You can enable it by setting the CI/CD Variable $TERRAFORM_POLICY_VALIDATE to "true" and providing the policy-library repo URL to $POLICY_LIBRARY_REPO variable. See the below for details on the Variables to be set on the CI/CD pipeline. - - -| Variable | | Example Value | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | -| TERRAFORM_VERSION | Version of Terraform to execute. Terraform will be installed at the beginning of every job. | 1.3.7 | -| PROJECT_NAME | The project containing the service account that has permission to communicate with the WIF provider. Should be created as part of Project Factory. | bitbucket-connect-prj | -| GCP_PROJECT_NUMBER | Project number for the project that hosts the WIF provider | 107999111999 | -| SERVICE_ACCOUNT_NAME | The name of the service account that will be used to deploy. Must be hosted in PROJECT_NAME. | bb-service-acct | -| BUCKET_PATH | A state bucket that will hold the terraform state. This bucket must previously exist and the service account must have permission to read/write to it. | gcs-state-bucket-name | -| POLICY_LIBRARY_REPO | https://github.com/GoogleCloudPlatform/policy-library | The public repo where the policies are hosted | -| TERRAFORM_POLICY_VALIDATE | This is a flag that should be set to “true” in order to enable the policy validation | Should be set to “true” to enable validation | -| workload_identity_pool_id | | bitbucket-test-pool | -| workload_identity_provider_id | | bitbucket-test-provider | | - -* Once the prerequisites are set up, any commit to the remote main branch with changes to *.tf, *.tfvars, data/*, modules/* files should trigger the pipeline. - - -* get_oidctoken.sh should run successfully in the pipeline if the workload identity federation is configured as required. - -### Pipeline Workflow Overview -The complete workflow comprises of 4-5 stages and 2 before-script jobs - * setup terraform : - * This step should download the terraform from internet and keep that as an artifacts to be used in later stages - * Stages: - * setup-terraform : Downloads the specified TF_VERSION and passes it as a binary to the next stages - * plan: Runs terraform plan and saves the plan and json version of the plan as artifacts, this depends on the branch - * policy-validate: Runs gcloud terraform vet against the terraform code with the constraints in the specified repository. - * apply: This is executed for specified list of branches, currently defined branches are prod/dev/staging +1. Clone this repository and push it to your own Bitbucket repository. +1. Enable pipelines in your Bitbucket Repository. This is done in **Repository Settings** > **Pipelines** > **Settings**. +2. Set up the required environment variables in your Bitbucket repository settings. This is done in **Repository Settings** > **Pipelines** > **Repository variables**. + + | Environment Variable | Description | Example Value | + |---------------------------------|-----------------------------------------------------------------------------------|-------------------------------------------------------------| + | `TERRAFORM_VERSION` | The version of Terraform you want to use | `1.4.2` | + | `STATE_BUCKET` | The Google Cloud Storage bucket where your Terraform state files will be stored | `my-terraform-state-bucket` | + | `GCP_WORKLOAD_IDENTITY_PROVIDER`| The fully qualified identifier of your Google Cloud Workload Identity Provider *(See **project-factory** outputs)* | `projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID` | + | `GCP_SERVICE_ACCOUNT` | The email address of your Google Cloud Service Account *(See **project-factory** outputs)* | `my-service-account@my-project.iam.gserviceaccount.com` | + | `PROJECT_NAME` | Your Google Cloud project ID *(See **project-factory** outputs)* | `my-gcp-project` | + | `TERRAFORM_POLICY_VALIDATE` | | `true`| +1. add a `terraform.tfvars` that specifies the project you want the bucket to be created in + ```hcl + project = "my-gcp-project" + ``` +1. Commit to your repository to trigger a build diff --git a/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/skunkworks/bitbucket-pipelines.yml similarity index 51% rename from examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml rename to examples/guardrails/bitbucket/skunkworks/bitbucket-pipelines.yml index 562d91b..44bd0e2 100644 --- a/examples/guardrails/bitbucket/project-factory/.bitbucket/bitbucket-pipelines.yml +++ b/examples/guardrails/bitbucket/skunkworks/bitbucket-pipelines.yml @@ -11,27 +11,63 @@ pipelines: - ls /usr/bin artifacts: - terraform - - step: - oidc: true - name: configure oidc - script: - - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" - - ./get_oidctoken.sh - artifacts: - - sts-creds.json - - /tmp/gcp_access_token.out - step: name: terraform init oidc: true script: - cp terraform /usr/bin/ - ls . /usr/bin/ - - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" - ./get_oidctoken.sh - - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json - - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/.gcp_temp_cred.json + - /usr/bin/terraform init -backend-config="bucket=${STATE_BUCKET}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" - /usr/bin/terraform plan branches: + feature/*: + - step: + name: setup terraform + script: + - apt-get update && apt-get install unzip wget -y + - wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip || echo fine... + - unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip + - yes | cp terraform /usr/bin/ + - ls /usr/bin + artifacts: + - terraform + - step: + name: main branch execution tf-apply + oidc: true + script: + - cp terraform /usr/bin/ + - ls . /usr/bin/ + - export SERVICE_ACCOUNT_EMAIL="${GCP_SERVICE_ACCOUNT}" + - ./get_oidctoken.sh + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/.gcp_temp_cred.json + - /usr/bin/terraform init -backend-config="bucket=${STATE_BUCKET}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - /usr/bin/terraform plan + - /usr/bin/terraform apply -auto-approve + dev: + - step: + name: setup terraform + script: + - apt-get update && apt-get install unzip wget -y + - wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip || echo fine... + - unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip + - yes | cp terraform /usr/bin/ + - ls /usr/bin + artifacts: + - terraform + - step: + name: dev branch execution tf-apply + oidc: true + script: + - cp terraform /usr/bin/ + - ls . /usr/bin/ + - export SERVICE_ACCOUNT_EMAIL="${GCP_SERVICE_ACCOUNT}" + - ./get_oidctoken.sh + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/.gcp_temp_cred.json + - /usr/bin/terraform init -backend-config="bucket=${STATE_BUCKET}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - /usr/bin/terraform plan + - /usr/bin/terraform apply -auto-approve main: - step: name: setup terraform @@ -53,12 +89,12 @@ pipelines: - export TF_ROOT=tf_root - mkdir -p $TF_ROOT - ls . /usr/bin/ - - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - export SERVICE_ACCOUNT_EMAIL="${GCP_SERVICE_ACCOUNT}" - ./get_oidctoken.sh - - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/.gcp_temp_cred.json - apt-get install google-cloud-sdk-terraform-tools -y - git clone $POLICY_LIBRARY_REPO $TF_ROOT/policy-repo - - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - /usr/bin/terraform init -backend-config="bucket=${STATE_BUCKET}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" - /usr/bin/terraform plan -out $TF_PLAN_NAME - /usr/bin/terraform show --json $TF_PLAN_NAME > $TF_ROOT/tfplan.json - violations=$(gcloud beta terraform vet $TF_ROOT/tfplan.json --policy-library=$TF_ROOT/policy-repo --format=json) @@ -77,15 +113,15 @@ pipelines: - fi - fi - step: - name: master branch execution tf-apply + name: prod branch execution tf-apply oidc: true script: - cp terraform /usr/bin/ - ls . /usr/bin/ - - export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_NAME}.iam.gserviceaccount.com" + - export SERVICE_ACCOUNT_EMAIL="${GCP_SERVICE_ACCOUNT}" - ./get_oidctoken.sh - - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json - - /usr/bin/terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" + - export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/.gcp_temp_cred.json + - /usr/bin/terraform init -backend-config="bucket=${STATE_BUCKET}" -backend-config="prefix=${BITBUCKET_REPO_FULL_NAME}" - /usr/bin/terraform plan - /usr/bin/terraform apply -auto-approve diff --git a/examples/guardrails/bitbucket/skunkworks/get_oidctoken.sh b/examples/guardrails/bitbucket/skunkworks/get_oidctoken.sh index b032b70..c91d00b 100755 --- a/examples/guardrails/bitbucket/skunkworks/get_oidctoken.sh +++ b/examples/guardrails/bitbucket/skunkworks/get_oidctoken.sh @@ -1,7 +1,8 @@ #!/bin/bash -echo -n "${BITBUCKET_STEP_OIDC_TOKEN}" > /tmp/gcp_access_token.out -gcloud iam workload-identity-pools create-cred-config "projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/${workload_identity_pool_id}/providers/${workload_identity_pool_provider_id}" --credential-source-file=/tmp/gcp_access_token.out --service-account="${SERVICE_ACCOUNT_EMAIL}" --output-file=sts-creds.json -export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json -gcloud auth login --cred-file=`pwd`/sts-creds.json +set -e +echo ${BITBUCKET_STEP_OIDC_TOKEN} > /tmp/gcp_access_token.out +gcloud iam workload-identity-pools create-cred-config ${GCP_WORKLOAD_IDENTITY_PROVIDER} --service-account="${GCP_SERVICE_ACCOUNT}" --output-file=.gcp_temp_cred.json --credential-source-file=/tmp/gcp_access_token.out +gcloud auth login --cred-file=`pwd`/.gcp_temp_cred.json +gcloud projects list gcloud config set project $PROJECT_NAME -gcloud services list \ No newline at end of file +export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/.gcp_temp_cred.json \ No newline at end of file diff --git a/examples/guardrails/bitbucket/skunkworks/terraform.tfvars b/examples/guardrails/bitbucket/skunkworks/terraform.tfvars new file mode 100644 index 0000000..a97285b --- /dev/null +++ b/examples/guardrails/bitbucket/skunkworks/terraform.tfvars @@ -0,0 +1 @@ +project = "bitbucketproject2-prj-4e3611b6" \ No newline at end of file From 62181693457a50ade829c01e9ff510d5022d93b1 Mon Sep 17 00:00:00 2001 From: arod0719 Date: Wed, 29 Mar 2023 15:51:15 +0000 Subject: [PATCH 132/132] project factory readme --- .../bitbucket/project-factory/README.md | 130 ++++++++++-------- 1 file changed, 75 insertions(+), 55 deletions(-) diff --git a/examples/guardrails/bitbucket/project-factory/README.md b/examples/guardrails/bitbucket/project-factory/README.md index f901e7f..99d6ae3 100644 --- a/examples/guardrails/bitbucket/project-factory/README.md +++ b/examples/guardrails/bitbucket/project-factory/README.md @@ -1,75 +1,95 @@ # Project Factory -This is a template for a DevOps project factory. +This repo will deploy new projects and the necessary resources to use Workload Identity Federation with Bitbucket. Inside the repo is a subdirectory structure `data/projects` that contain yaml files. Each yaml file contains the definition of a new project. Each project deployed will also create a new Workload Identity Pool and provider. The created pool ID and provider are included in the terraform outputs. -It can be used with https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory (https://github.com/google/devops-governance/tree/main/examples/guardrails/folder-factory) and is intended to house the projects of a specified folder: +This repo is intend to be deployed manually. -Overview +## Deployment -Using Keyless Authentication the project factory connects a defined Github repository with a target service account and project within GCP for IaC. +### Authentication -![Folder Factory](https://user-images.githubusercontent.com/94000358/169809882-f5ff9fb1-d037-49de-8c2c-bf0d457b662f.png) +project-factory should be deployed outside of the normal pipeline process. Workload Identity Federation, which will authorize Bitbucket pipelines to communicate with GCP, will not be configured until after this repository is fully deployed. -The idea is to enable developers of the "skunkworks" repository to deploy into the "skunkworks" project via IaC pipelines on Github. +Since this is being deployed outside of the pipeline environment, authentication will need to be established separately. If deploying locally, this can be done with -## Repository Configuration -This repository does not need any additional runners (uses Github runners) and does require you to previously setup Workload Identity Federation to authenticate. +```bash +gcloud auth login +``` + +Alternatively, [CloudShell](https://cloud.google.com/shell) can be used for an easy environment that is already GCP authenticated. + +The user who is authenticated should have permissions in GCP IAM to create projects and attach them to the correct billing account. Additionally, the user needs the permissions to create and manage Workload Identity Federation pools and providers. If desired, a Service Account may be used that has the required permissions. See [Impersonating Service Accounts](https://cloud.google.com/iam/docs/impersonating-service-accounts) for more information. + + +### Terraform + +After authentication, the Terraform can be deployed. Navigate to the root of the project-factory repository. -If you do require additional assitance to setup Workload Identity Federation have a look at: https://www.youtube.com/watch?v=BuyoENMmtVw +```bash +cd project-factory +``` -After setting up WIF you can then go ahead and configure this repository. This can be done by either with setting the following secrets: +Before Terraform can be initialized, Terraform needs to be directed on where to store the state file within Google Cloud Storage. Open the file named `providers.tf` and populate values for bucket and prefix. The modified file should resemble the following: -Secret settings +```hcl +terraform { + backend "gcs" { + bucket = "your-bucket-name" + prefix = "path/to/state/file/" + } +} -or by modifing the [Workflow Action](.github/workflows/terraform-deployment.yml) and setting the environment variables: +# ... ``` -env: - STATE_BUCKET: 'XXXX' - # The GCS bucket to store the terraform state - FOLDER: 'folders/XXXX' - # The folder under which the projects should be created - WORKLOAD_IDENTITY_PROVIDER: 'projects/XXXX' - # The workload identity provider that should be used for this repository. - SERVICE_ACCOUNT: 'XXXX@XXXX' - # The service account that should be used for this repository. + +The bucket value should be the name of a bucket already in GCP. The prefix value is optional, but can be used to define a directory structure within the bucket to store the state file. + +After this change is made, Terraform can be initialized. + +```bash +terraform init ``` -## Setting up projects +Here is where you would want to add, change, or modify any of the yaml files in the `data/projects` directory. For more information, see the sample yaml provided. Each yaml file contains the definition of a new GCP project. + +Before the Terraform can be applied, there is one more section that will require information. In the root of the project-factory repo there must exist a file named terraform.tfvars. This is where variables need to be populated that apply to all projects. The following values need to be provided: + +| Variable | Description | Example Value | +|-|-|-| +|`folder`|Folder ID of where to deploy the project |`"folders/98765432101"`| +|`folder`|GCP billing account to attach projects to = |`"018888-01888-ABC123"`| +|`folder`|Name of workspace in Bitbucket |`"bbworkspace"`| +|`folder`|List of audience tokens provided by Bitbucket. See below for additional details. |`["ari:cloud:bitbucket::workspace/000000ee-1111-11ae-bbbb-1111aeeee111"]`| + +#### `allowed_audiences` Value + +The audience token ensures Bitbucket pipelines are allowed to authenticate via Workload Identity. Its value can be retrieved after creating a repository. It is important to note that the “audience” value is tied to the Bitbucket Workspace, meaning all repos in the same Bitbucket workspace will have the same audience value. If an organization uses multiple workspaces, the audience value will need to be retrieved for each. + +For the purposes of this example, we will create a repo in Bitbucket “skunkworks” that will later be populated with terraform. + +In bitbucket, create the new repository. Aftwards, Bitbucket pipelines will need to be enabled. Go to the repository settings and look for **Pipelines** > **Settings** and check **Enable Pipelines**. + +Next, navigate to **Pipelines** > **OpenID Connect**. This screen will contain a value, **Audience**, which we will need to copy. The value in Audience needs to be included in our list within `terraform.tfvars`. + +A complete `terraform.tfvars` should resemble the following: + +```hcl +folder = "folders/00000012345" + +billing_account = "018888-01888-ABC123" + +workspace = "bbworkspace" + +allowed_audiences = ["ari:cloud:bitbucket::workspace/000000ee-1111-11ae-bbbb-1111aeeee111", "ari:cloud:bitbucket::workspace/000000ee-2222-11ae-bbbb-2222affff111"] +``` -The project factory will: -- create a service account with defined rights -- create a project within the folder -- connect the service account to the Github repository informantion +Upon making any modifications, run the following in the root of the repo to plan the deployment. -It uses YAML configuration files for every project with the following sample structure: +```bash +terraform plan ``` -billing_account_id: XXXXXX-XXXXXX-XXXXXX -roles: - - roles/viewer - - roles/iam.serviceAccountUser - - roles/iam.securityReviewer - - roles/monitoring.viewer - - roles/monitoring.editor - - roles/monitoring.alertPolicyViewer - - roles/monitoring.alertPolicyEditor - - roles/monitoring.dashboardViewer - - roles/monitoring.dashboardEditor - - roles/monitoring.notificationChannelViewer - - roles/monitoring.notificationChannelEditor - - roles/monitoring.servicesViewer - - roles/monitoring.servicesEditor - - roles/monitoring.uptimeCheckConfigViewer - - roles/monitoring.uptimeCheckConfigEditor - - roles/secretmanager.viewer - - roles/secretmanager.secretVersionManager - - roles/secretmanager.admin - - roles/storage.admin - - roles/storage.objectAdmin - - roles/storage.objectCreator - - roles/storage.objectViewer -repo_provider: github -repo_name: devops-governance/skunkworks -repo_branch: dev +After reviewing the proposed infrastructure changes, approve the deployment. +```bash +terraform apply -auto-approve ``` -Every project is defined with its own file located in the [Project Folder](data/projects).