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/README.md b/README.md index bc7663f..44e3dcf 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,65 @@ 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: -![Guardrail Examples](https://user-images.githubusercontent.com/94000358/169811919-e5c36181-c1d2-4339-8103-d86640e9a1f1.png) +Guardrail Examples -- The [Folder Factory](/examples/guardrails/folder-factory) 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/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 [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 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* 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 + +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 + +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. + +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 + +### 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](/examples/guardrails) section covers different CICD tools and how to leverage Workload Identity Federation. + +## 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. diff --git a/examples/guardrails/azuredevops/README.md b/examples/guardrails/azuredevops/README.md new file mode 100644 index 0000000..1eab061 --- /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](../../../examples/guardrails/azuredevops/folder-factory) 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/azuredevops/folder-factory) diff --git a/examples/guardrails/folder-factory/.github/workflows/terraform-deployment.yml b/examples/guardrails/azuredevops/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/azuredevops/folder-factory/.github/workflows/terraform-deployment.yml diff --git a/examples/guardrails/folder-factory/.gitignore b/examples/guardrails/azuredevops/folder-factory/.gitignore similarity index 100% rename from examples/guardrails/folder-factory/.gitignore rename to examples/guardrails/azuredevops/folder-factory/.gitignore diff --git a/examples/guardrails/azuredevops/folder-factory/README.md b/examples/guardrails/azuredevops/folder-factory/README.md new file mode 100644 index 0000000..58cbb56 --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/README.md @@ -0,0 +1,96 @@ +# 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 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. + + +## 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/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/folder-factory/azure-pipeline.yml b/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml new file mode 100644 index 0000000..e7e0a80 --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/azure-pipeline.yml @@ -0,0 +1,146 @@ +name: "$(Date:yyyyMMdd)$(Rev:.r)" + +trigger: + branches: + include: + - none + + +variables: + 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: + vmImage: "ubuntu-latest" + + +stages: + - stage: auth + displayName: "GCP WIF Auth" + jobs: + - job: governance_pipeline_azure + timeoutInMinutes: 30 + 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: + azureserviceconnection: "$(azureserviceconnection)" + 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 @- < ./tfplan.json + + - task: AzureCLI@2 + displayName: "Policy Validate" + condition: eq(variables.policyValidate,'true') + inputs: + azureserviceconnection: "$(azureserviceconnection)" + 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: + azureserviceconnection: "$(azureserviceconnection)" + 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/folder-factory/backend.tf b/examples/guardrails/azuredevops/folder-factory/backend.tf new file mode 100644 index 0000000..6628545 --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/backend.tf @@ -0,0 +1,7 @@ +terraform { + backend "gcs" { + bucket = "terraform-backend-bucket-azuredevops" + prefix = "bucket-backend" + + } +} \ No newline at end of file diff --git a/examples/guardrails/azuredevops/folder-factory/data/folders/app1.yaml b/examples/guardrails/azuredevops/folder-factory/data/folders/app1.yaml new file mode 100644 index 0000000..359bd24 --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/data/folders/app1.yaml @@ -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. + +parent: organizations/1079505589501 +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:azure-sa@azuretogcp-374609.iam.gserviceaccount.com \ No newline at end of file diff --git a/examples/guardrails/azuredevops/folder-factory/data/folders/dev.yaml b/examples/guardrails/azuredevops/folder-factory/data/folders/dev.yaml new file mode 100644 index 0000000..f99e4d5 --- /dev/null +++ b/examples/guardrails/azuredevops/folder-factory/data/folders/dev.yaml @@ -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. +parent: folders/71282294003 +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:jenkins-wif@prd-project-371507.iam.gserviceaccount.com diff --git a/examples/guardrails/folder-factory/data/folders/folder.yaml.sample b/examples/guardrails/azuredevops/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/azuredevops/folder-factory/data/folders/folder.yaml.sample diff --git a/examples/guardrails/folder-factory/main.tf b/examples/guardrails/azuredevops/folder-factory/main.tf similarity index 100% rename from examples/guardrails/folder-factory/main.tf rename to examples/guardrails/azuredevops/folder-factory/main.tf diff --git a/examples/guardrails/folder-factory/modules/folder/README.md b/examples/guardrails/azuredevops/folder-factory/modules/folder/README.md similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/README.md rename to examples/guardrails/azuredevops/folder-factory/modules/folder/README.md diff --git a/examples/guardrails/folder-factory/modules/folder/firewall-policies.tf b/examples/guardrails/azuredevops/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/azuredevops/folder-factory/modules/folder/firewall-policies.tf diff --git a/examples/guardrails/folder-factory/modules/folder/iam.tf b/examples/guardrails/azuredevops/folder-factory/modules/folder/iam.tf similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/iam.tf rename to examples/guardrails/azuredevops/folder-factory/modules/folder/iam.tf diff --git a/examples/guardrails/folder-factory/modules/folder/logging.tf b/examples/guardrails/azuredevops/folder-factory/modules/folder/logging.tf similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/logging.tf rename to examples/guardrails/azuredevops/folder-factory/modules/folder/logging.tf diff --git a/examples/guardrails/folder-factory/modules/folder/main.tf b/examples/guardrails/azuredevops/folder-factory/modules/folder/main.tf similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/main.tf rename to examples/guardrails/azuredevops/folder-factory/modules/folder/main.tf diff --git a/examples/guardrails/folder-factory/modules/folder/organization-policies.tf b/examples/guardrails/azuredevops/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/azuredevops/folder-factory/modules/folder/organization-policies.tf diff --git a/examples/guardrails/folder-factory/modules/folder/outputs.tf b/examples/guardrails/azuredevops/folder-factory/modules/folder/outputs.tf similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/outputs.tf rename to examples/guardrails/azuredevops/folder-factory/modules/folder/outputs.tf diff --git a/examples/guardrails/folder-factory/modules/folder/tags.tf b/examples/guardrails/azuredevops/folder-factory/modules/folder/tags.tf similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/tags.tf rename to examples/guardrails/azuredevops/folder-factory/modules/folder/tags.tf diff --git a/examples/guardrails/folder-factory/modules/folder/variables.tf b/examples/guardrails/azuredevops/folder-factory/modules/folder/variables.tf similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/variables.tf rename to examples/guardrails/azuredevops/folder-factory/modules/folder/variables.tf diff --git a/examples/guardrails/folder-factory/modules/folder/versions.tf b/examples/guardrails/azuredevops/folder-factory/modules/folder/versions.tf similarity index 100% rename from examples/guardrails/folder-factory/modules/folder/versions.tf rename to examples/guardrails/azuredevops/folder-factory/modules/folder/versions.tf 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/project-factory/.github/workflows/terraform-deployment.yml b/examples/guardrails/azuredevops/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/azuredevops/project-factory/.github/workflows/terraform-deployment.yml diff --git a/examples/guardrails/project-factory/.gitignore b/examples/guardrails/azuredevops/project-factory/.gitignore similarity index 100% rename from examples/guardrails/project-factory/.gitignore rename to examples/guardrails/azuredevops/project-factory/.gitignore diff --git a/examples/guardrails/azuredevops/project-factory/README.md b/examples/guardrails/azuredevops/project-factory/README.md new file mode 100644 index 0000000..aef58d4 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/README.md @@ -0,0 +1,105 @@ +# 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/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. + +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. + + +## 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 +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/project-factory/azure-pipeline.yml b/examples/guardrails/azuredevops/project-factory/azure-pipeline.yml new file mode 100644 index 0000000..0c1c9d1 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/azure-pipeline.yml @@ -0,0 +1,146 @@ +name: "$(Date:yyyyMMdd)$(Rev:.r)" + +trigger: + branches: + include: + - none + + +variables: + 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: + vmImage: "ubuntu-latest" + + +stages: + - stage: auth + displayName: "GCP WIF Auth" + jobs: + - job: governance_pipeline_azure + timeoutInMinutes: 30 + 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: + azureserviceconnection: "$(azureserviceconnection)" + 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 @- < ./tfplan.json + + - task: AzureCLI@2 + displayName: "Policy Validate" + condition: eq(variables.policyValidate,'true') + inputs: + azureserviceconnection: "$(azureserviceconnection)" + 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 + + 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: + azureserviceconnection: "$(azureserviceconnection)" + 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/project-factory/backend.tf b/examples/guardrails/azuredevops/project-factory/backend.tf new file mode 100644 index 0000000..c349d11 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/backend.tf @@ -0,0 +1,7 @@ +terraform { + backend "gcs" { + bucket = "terraform-backend-bucket-azuredevops" + prefix = "projectbucket-backend" + + } +} \ No newline at end of file diff --git a/examples/guardrails/azuredevops/project-factory/data/projects/dev-skunkworks.yaml b/examples/guardrails/azuredevops/project-factory/data/projects/dev-skunkworks.yaml new file mode 100644 index 0000000..e5e07cf --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/data/projects/dev-skunkworks.yaml @@ -0,0 +1,28 @@ +billing_account_id: 01EDB8-9273DF-AA9623 + +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: azuredevops +repo_name: devops-governance.git +repo_branch: dev diff --git a/examples/guardrails/azuredevops/project-factory/data/projects/prod-skunkworks.yaml b/examples/guardrails/azuredevops/project-factory/data/projects/prod-skunkworks.yaml new file mode 100644 index 0000000..5122bd2 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/data/projects/prod-skunkworks.yaml @@ -0,0 +1,28 @@ +billing_account_id: 01EDB8-9273DF-AA9623 + +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: azuredevops +repo_name: devops-governance.git +repo_branch: main \ No newline at end of file diff --git a/examples/guardrails/project-factory/data/projects/project.yaml.sample b/examples/guardrails/azuredevops/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/azuredevops/project-factory/data/projects/project.yaml.sample diff --git a/examples/guardrails/azuredevops/project-factory/data/projects/stage-skunkworks.yaml b/examples/guardrails/azuredevops/project-factory/data/projects/stage-skunkworks.yaml new file mode 100644 index 0000000..a870292 --- /dev/null +++ b/examples/guardrails/azuredevops/project-factory/data/projects/stage-skunkworks.yaml @@ -0,0 +1,28 @@ +billing_account_id: 01EDB8-9273DF-AA9623 + +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: azuredevops +repo_name: devops-governance.git +repo_branch: staging \ No newline at end of file diff --git a/examples/guardrails/project-factory/main.tf b/examples/guardrails/azuredevops/project-factory/main.tf similarity index 100% rename from examples/guardrails/project-factory/main.tf rename to examples/guardrails/azuredevops/project-factory/main.tf diff --git a/examples/guardrails/project-factory/modules/project/README.md b/examples/guardrails/azuredevops/project-factory/modules/project/README.md similarity index 100% rename from examples/guardrails/project-factory/modules/project/README.md rename to examples/guardrails/azuredevops/project-factory/modules/project/README.md diff --git a/examples/guardrails/project-factory/modules/project/iam.tf b/examples/guardrails/azuredevops/project-factory/modules/project/iam.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/iam.tf rename to examples/guardrails/azuredevops/project-factory/modules/project/iam.tf diff --git a/examples/guardrails/project-factory/modules/project/logging.tf b/examples/guardrails/azuredevops/project-factory/modules/project/logging.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/logging.tf rename to examples/guardrails/azuredevops/project-factory/modules/project/logging.tf diff --git a/examples/guardrails/project-factory/modules/project/main.tf b/examples/guardrails/azuredevops/project-factory/modules/project/main.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/main.tf rename to examples/guardrails/azuredevops/project-factory/modules/project/main.tf diff --git a/examples/guardrails/project-factory/modules/project/organization-policies.tf b/examples/guardrails/azuredevops/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/azuredevops/project-factory/modules/project/organization-policies.tf diff --git a/examples/guardrails/project-factory/modules/project/outputs.tf b/examples/guardrails/azuredevops/project-factory/modules/project/outputs.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/outputs.tf rename to examples/guardrails/azuredevops/project-factory/modules/project/outputs.tf diff --git a/examples/guardrails/project-factory/modules/project/service-accounts.tf b/examples/guardrails/azuredevops/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/azuredevops/project-factory/modules/project/service-accounts.tf diff --git a/examples/guardrails/project-factory/modules/project/shared-vpc.tf b/examples/guardrails/azuredevops/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/azuredevops/project-factory/modules/project/shared-vpc.tf diff --git a/examples/guardrails/project-factory/modules/project/tags.tf b/examples/guardrails/azuredevops/project-factory/modules/project/tags.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/tags.tf rename to examples/guardrails/azuredevops/project-factory/modules/project/tags.tf diff --git a/examples/guardrails/project-factory/modules/project/variables.tf b/examples/guardrails/azuredevops/project-factory/modules/project/variables.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/variables.tf rename to examples/guardrails/azuredevops/project-factory/modules/project/variables.tf diff --git a/examples/guardrails/project-factory/modules/project/versions.tf b/examples/guardrails/azuredevops/project-factory/modules/project/versions.tf similarity index 100% rename from examples/guardrails/project-factory/modules/project/versions.tf rename to examples/guardrails/azuredevops/project-factory/modules/project/versions.tf diff --git a/examples/guardrails/project-factory/modules/project/vpc-sc.tf b/examples/guardrails/azuredevops/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/azuredevops/project-factory/modules/project/vpc-sc.tf diff --git a/examples/guardrails/project-factory/modules/project_plus/README.md b/examples/guardrails/azuredevops/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/azuredevops/project-factory/modules/project_plus/README.md diff --git a/examples/guardrails/project-factory/modules/project_plus/main.tf b/examples/guardrails/azuredevops/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/azuredevops/project-factory/modules/project_plus/main.tf diff --git a/examples/guardrails/project-factory/modules/project_plus/outputs.tf b/examples/guardrails/azuredevops/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/azuredevops/project-factory/modules/project_plus/outputs.tf diff --git a/examples/guardrails/project-factory/modules/project_plus/variables.tf b/examples/guardrails/azuredevops/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/azuredevops/project-factory/modules/project_plus/variables.tf diff --git a/examples/guardrails/project-factory/modules/project_plus/versions.tf b/examples/guardrails/azuredevops/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/azuredevops/project-factory/modules/project_plus/versions.tf diff --git a/examples/guardrails/project-factory/outputs.tf b/examples/guardrails/azuredevops/project-factory/outputs.tf similarity index 100% rename from examples/guardrails/project-factory/outputs.tf rename to examples/guardrails/azuredevops/project-factory/outputs.tf 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/skunkworks/.github/workflows/tf-actions-dev.yml b/examples/guardrails/azuredevops/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/azuredevops/skunkworks/.github/workflows/tf-actions-dev.yml diff --git a/examples/guardrails/skunkworks/.github/workflows/tf-actions-prod.yml b/examples/guardrails/azuredevops/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/azuredevops/skunkworks/.github/workflows/tf-actions-prod.yml diff --git a/examples/guardrails/skunkworks/.github/workflows/tf-actions-stage.yml b/examples/guardrails/azuredevops/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/azuredevops/skunkworks/.github/workflows/tf-actions-stage.yml diff --git a/examples/guardrails/azuredevops/skunkworks/README.md b/examples/guardrails/azuredevops/skunkworks/README.md new file mode 100644 index 0000000..6caff47 --- /dev/null +++ b/examples/guardrails/azuredevops/skunkworks/README.md @@ -0,0 +1,61 @@ +# Skunkworks - IaC Kickstarter Template + +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 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. + +## 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. + + +| 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 | | + +## 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 + +* 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. 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 new file mode 100644 index 0000000..628a479 --- /dev/null +++ b/examples/guardrails/azuredevops/skunkworks/azure-pipeline.yml @@ -0,0 +1,146 @@ +name: "$(Date:yyyyMMdd)$(Rev:.r)" + +trigger: + branches: + include: + - none + + +variables: + 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: + vmImage: "ubuntu-latest" + + +stages: + - stage: auth + displayName: "GCP WIF Auth" + jobs: + - job: governance_pipeline_azure + timeoutInMinutes: 30 + 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: + azureserviceconnection: "$(azureserviceconnection)" + 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 @- < ./tfplan.json + + - task: AzureCLI@2 + displayName: "Policy Validate" + condition: eq(variables.policyValidate,'true') + inputs: + azureserviceconnection: "$(azureserviceconnection)" + scriptType: "bash" + scriptLocation: "inlineScript" + workingDirectory: "examples/guardrails/skunkworks" + 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: + azureserviceconnection: "$(azureserviceconnection)" + scriptType: "bash" + scriptLocation: "inlineScript" + workingDirectory: "examples/guardrails/skunkworks" + 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/skunkworks/main.tf b/examples/guardrails/azuredevops/skunkworks/main.tf similarity index 100% rename from examples/guardrails/skunkworks/main.tf rename to examples/guardrails/azuredevops/skunkworks/main.tf diff --git a/examples/guardrails/azuredevops/skunkworks/provider.tf b/examples/guardrails/azuredevops/skunkworks/provider.tf new file mode 100644 index 0000000..22aec50 --- /dev/null +++ b/examples/guardrails/azuredevops/skunkworks/provider.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. + */ + +terraform { + # backend "gcs" { + # bucket = "terraform-backend-bucket-azuredevops" + # prefix = "state" + # } + required_providers { + google = { + source = "hashicorp/google" + version = "~>4.11" + } + } +} + +provider "google" { + +} + +provider "google-beta" { + +} \ No newline at end of file diff --git a/examples/guardrails/azuredevops/skunkworks/terraform.tfvars b/examples/guardrails/azuredevops/skunkworks/terraform.tfvars new file mode 100644 index 0000000..2060445 --- /dev/null +++ b/examples/guardrails/azuredevops/skunkworks/terraform.tfvars @@ -0,0 +1 @@ +project = "dev-skunkworks-prj-48d166c5" \ No newline at end of file diff --git a/examples/guardrails/skunkworks/variables.tf b/examples/guardrails/azuredevops/skunkworks/variables.tf similarity index 100% rename from examples/guardrails/skunkworks/variables.tf rename to examples/guardrails/azuredevops/skunkworks/variables.tf diff --git a/examples/guardrails/bitbucket/README.md b/examples/guardrails/bitbucket/README.md new file mode 100644 index 0000000..696677c --- /dev/null +++ b/examples/guardrails/bitbucket/README.md @@ -0,0 +1,43 @@ +# 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: + +Bitbucket + +- 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 + +## 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 + - **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/.gitignore b/examples/guardrails/bitbucket/folder-factory/.gitignore new file mode 100644 index 0000000..a5d8c0e --- /dev/null +++ b/examples/guardrails/bitbucket/folder-factory/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.terraform +.terraform.lock.hcl diff --git a/examples/guardrails/bitbucket/folder-factory/README.md b/examples/guardrails/bitbucket/folder-factory/README.md new file mode 100644 index 0000000..8c4b00b --- /dev/null +++ b/examples/guardrails/bitbucket/folder-factory/README.md @@ -0,0 +1,62 @@ +# 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. + +## Deployment: + +#### Authentication + +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. + +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: + +``` +gcloud auth login +``` + +Alternatively, CloudShell can be used for an easy environment that is already GCP authenticated. + +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. + +#### Terraform + +After authentication, the terraform can be deployed. Navigate to the root of the folder-factory repository and initialize the terraform. + +``` +cd folder-factory +``` + +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: + +``` +terraform { +  backend "gcs" { +    bucket = "your-bucket-name" +    prefix = "path/to/state/file/" +  } +} +#  ... +``` + +After this change is made, Terraform can be initialized. + +``` +terraform init +``` + +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. + +Upon making any modifications, run the following to plan the deployment. + +``` +terraform plan +``` + +After reviewing the proposed infrastructure changes, approve the deployment. + +``` +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/data/folders/folder.yaml.sample b/examples/guardrails/bitbucket/folder-factory/data/folders/folder.yaml.sample new file mode 100644 index 0000000..9ea745d --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/folder-factory/main.tf b/examples/guardrails/bitbucket/folder-factory/main.tf new file mode 100644 index 0000000..d024c17 --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/folder-factory/modules/folder/README.md b/examples/guardrails/bitbucket/folder-factory/modules/folder/README.md new file mode 100644 index 0000000..4f6898b --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/folder-factory/modules/folder/firewall-policies.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/firewall-policies.tf new file mode 100644 index 0000000..96224c5 --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/folder-factory/modules/folder/iam.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/iam.tf new file mode 100644 index 0000000..52886ba --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/folder-factory/modules/folder/logging.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/logging.tf new file mode 100644 index 0000000..d6a195e --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/folder-factory/modules/folder/main.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/main.tf new file mode 100644 index 0000000..5d285d2 --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/folder-factory/modules/folder/organization-policies.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/organization-policies.tf new file mode 100644 index 0000000..177a3d8 --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/folder-factory/modules/folder/outputs.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/outputs.tf new file mode 100644 index 0000000..37babc6 --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/folder-factory/modules/folder/tags.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/tags.tf new file mode 100644 index 0000000..2cd2f2f --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/folder-factory/modules/folder/variables.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/variables.tf new file mode 100644 index 0000000..a3f32e3 --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/folder-factory/modules/folder/versions.tf b/examples/guardrails/bitbucket/folder-factory/modules/folder/versions.tf new file mode 100644 index 0000000..e72a780 --- /dev/null +++ b/examples/guardrails/bitbucket/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/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/bitbucket/folder-factory/provider.tf b/examples/guardrails/bitbucket/folder-factory/provider.tf new file mode 100644 index 0000000..73f8561 --- /dev/null +++ b/examples/guardrails/bitbucket/folder-factory/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. + */ + +terraform { + backend "gcs" { + bucket = "" + prefix = "" #ex: bucket/folder-factory + } +} + +provider "google" { + +} + +provider "google-beta" { + +} 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/.gitignore b/examples/guardrails/bitbucket/project-factory/.gitignore new file mode 100644 index 0000000..a5d8c0e --- /dev/null +++ b/examples/guardrails/bitbucket/project-factory/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.terraform +.terraform.lock.hcl diff --git a/examples/guardrails/bitbucket/project-factory/README.md b/examples/guardrails/bitbucket/project-factory/README.md new file mode 100644 index 0000000..99d6ae3 --- /dev/null +++ b/examples/guardrails/bitbucket/project-factory/README.md @@ -0,0 +1,95 @@ +# 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. + +This repo is intend to be deployed manually. + +## Deployment + +### Authentication + +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. + +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 + +```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. + +```bash +cd project-factory +``` + +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: + +```hcl +terraform { + backend "gcs" { + bucket = "your-bucket-name" + prefix = "path/to/state/file/" + } +} + +# ... +``` + +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 +``` + +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"] +``` + +Upon making any modifications, run the following in the root of the repo to plan the deployment. + +```bash +terraform plan +``` +After reviewing the proposed infrastructure changes, approve the deployment. +```bash +terraform apply -auto-approve +``` + 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/data/projects/project.yaml.sample b/examples/guardrails/bitbucket/project-factory/data/projects/project.yaml.sample new file mode 100644 index 0000000..d813f4b --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/project-factory/main.tf b/examples/guardrails/bitbucket/project-factory/main.tf new file mode 100644 index 0000000..29d70e0 --- /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 = 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/modules/project/README.md b/examples/guardrails/bitbucket/project-factory/modules/project/README.md new file mode 100644 index 0000000..e4f2139 --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/project-factory/modules/project/iam.tf b/examples/guardrails/bitbucket/project-factory/modules/project/iam.tf new file mode 100644 index 0000000..69925cc --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/project-factory/modules/project/logging.tf b/examples/guardrails/bitbucket/project-factory/modules/project/logging.tf new file mode 100644 index 0000000..04d7abf --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/project-factory/modules/project/main.tf b/examples/guardrails/bitbucket/project-factory/modules/project/main.tf new file mode 100644 index 0000000..9f0dff4 --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/project-factory/modules/project/organization-policies.tf b/examples/guardrails/bitbucket/project-factory/modules/project/organization-policies.tf new file mode 100644 index 0000000..6870754 --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/project-factory/modules/project/outputs.tf b/examples/guardrails/bitbucket/project-factory/modules/project/outputs.tf new file mode 100644 index 0000000..10d0e55 --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/project-factory/modules/project/service-accounts.tf b/examples/guardrails/bitbucket/project-factory/modules/project/service-accounts.tf new file mode 100644 index 0000000..3423524 --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/project-factory/modules/project/shared-vpc.tf b/examples/guardrails/bitbucket/project-factory/modules/project/shared-vpc.tf new file mode 100644 index 0000000..9c7bd71 --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/project-factory/modules/project/tags.tf b/examples/guardrails/bitbucket/project-factory/modules/project/tags.tf new file mode 100644 index 0000000..683143b --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/project-factory/modules/project/variables.tf b/examples/guardrails/bitbucket/project-factory/modules/project/variables.tf new file mode 100644 index 0000000..578f9d2 --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/project-factory/modules/project/versions.tf b/examples/guardrails/bitbucket/project-factory/modules/project/versions.tf new file mode 100644 index 0000000..e72a780 --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/project-factory/modules/project/vpc-sc.tf b/examples/guardrails/bitbucket/project-factory/modules/project/vpc-sc.tf new file mode 100644 index 0000000..edaa203 --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/project-factory/modules/project_plus/README.md b/examples/guardrails/bitbucket/project-factory/modules/project_plus/README.md new file mode 100644 index 0000000..2976a30 --- /dev/null +++ b/examples/guardrails/bitbucket/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/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/bitbucket/project-factory/modules/project_plus/outputs.tf b/examples/guardrails/bitbucket/project-factory/modules/project_plus/outputs.tf new file mode 100644 index 0000000..c589d87 --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/project-factory/modules/project_plus/variables.tf b/examples/guardrails/bitbucket/project-factory/modules/project_plus/variables.tf new file mode 100644 index 0000000..67524ae --- /dev/null +++ b/examples/guardrails/bitbucket/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/bitbucket/project-factory/modules/project_plus/versions.tf b/examples/guardrails/bitbucket/project-factory/modules/project_plus/versions.tf new file mode 100644 index 0000000..2904126 --- /dev/null +++ b/examples/guardrails/bitbucket/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/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/bitbucket/project-factory/provider.tf b/examples/guardrails/bitbucket/project-factory/provider.tf new file mode 100644 index 0000000..d37bb09 --- /dev/null +++ b/examples/guardrails/bitbucket/project-factory/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. + */ + +terraform { + backend "gcs" { + bucket = "" + prefix = "" #ex: bucket/project-factory + } +} + +provider "google" { + +} + +provider "google-beta" { + +} 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 = [" **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/skunkworks/bitbucket-pipelines.yml b/examples/guardrails/bitbucket/skunkworks/bitbucket-pipelines.yml new file mode 100644 index 0000000..44bd0e2 --- /dev/null +++ b/examples/guardrails/bitbucket/skunkworks/bitbucket-pipelines.yml @@ -0,0 +1,128 @@ +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/ + - ./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 + 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 + 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="${GCP_SERVICE_ACCOUNT}" + - ./get_oidctoken.sh + - 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=${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) + - 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="${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 + + diff --git a/examples/guardrails/bitbucket/skunkworks/get_oidctoken.sh b/examples/guardrails/bitbucket/skunkworks/get_oidctoken.sh new file mode 100755 index 0000000..c91d00b --- /dev/null +++ b/examples/guardrails/bitbucket/skunkworks/get_oidctoken.sh @@ -0,0 +1,8 @@ +#!/bin/bash +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 +export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/.gcp_temp_cred.json \ 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/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 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/README.md b/examples/guardrails/cloudbuild/README.md new file mode 100644 index 0000000..a8ac851 --- /dev/null +++ b/examples/guardrails/cloudbuild/README.md @@ -0,0 +1,89 @@ +# Cloudbuild guardrail & pipeline example for individual workloads + +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 +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. + +### Setup 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. + * 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): + +![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. + +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](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. 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/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/cloudbuild/folder-factory/README.md b/examples/guardrails/cloudbuild/folder-factory/README.md new file mode 100644 index 0000000..4cb7699 --- /dev/null +++ b/examples/guardrails/cloudbuild/folder-factory/README.md @@ -0,0 +1,29 @@ +# Folder Factory + +The folder factory will: +- create folder(s) with defined organization 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/folders). + +> Detailed instructions of CloudBuild trigger setup can be found at [README](./../README.md). 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/folder-factory/provider.tf b/examples/guardrails/cloudbuild/folder-factory/provider.tf similarity index 100% rename from examples/guardrails/folder-factory/provider.tf rename to examples/guardrails/cloudbuild/folder-factory/provider.tf 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/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..c6a1857 --- /dev/null +++ b/examples/guardrails/cloudbuild/project-factory/README.md @@ -0,0 +1,41 @@ +# Project Factory + +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). + +> Detailed instructions of CloudBuild trigger setup can be found at [README](./../README.md). 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/cloudbuild/project-factory/main.tf b/examples/guardrails/cloudbuild/project-factory/main.tf new file mode 100644 index 0000000..3d589d4 --- /dev/null +++ b/examples/guardrails/cloudbuild/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/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/cloudbuild/project-factory/modules/project_plus/main.tf b/examples/guardrails/cloudbuild/project-factory/modules/project_plus/main.tf new file mode 100644 index 0000000..7330dd0 --- /dev/null +++ b/examples/guardrails/cloudbuild/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/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/cloudbuild/project-factory/outputs.tf b/examples/guardrails/cloudbuild/project-factory/outputs.tf new file mode 100644 index 0000000..43d9c4f --- /dev/null +++ b/examples/guardrails/cloudbuild/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/project-factory/provider.tf b/examples/guardrails/cloudbuild/project-factory/provider.tf similarity index 100% rename from examples/guardrails/project-factory/provider.tf rename to examples/guardrails/cloudbuild/project-factory/provider.tf diff --git a/examples/guardrails/cloudbuild/project-factory/variables.tf b/examples/guardrails/cloudbuild/project-factory/variables.tf new file mode 100644 index 0000000..6b8a34e --- /dev/null +++ b/examples/guardrails/cloudbuild/project-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 "folder" { + type = string + description = "Folder where projects will be created" +} + +variable "billing_account" { + type = string + description = "GCP Billing Account" +} diff --git a/examples/guardrails/cloudbuild/project-factory/wif.tf b/examples/guardrails/cloudbuild/project-factory/wif.tf new file mode 100644 index 0000000..d772d7b --- /dev/null +++ b/examples/guardrails/cloudbuild/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 = var.billing_account +} + +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/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/cloudbuild/skunkworks/README.md b/examples/guardrails/cloudbuild/skunkworks/README.md new file mode 100644 index 0000000..a6bb8ce --- /dev/null +++ b/examples/guardrails/cloudbuild/skunkworks/README.md @@ -0,0 +1,5 @@ +# Skunkworks - IaC Kickstarter Template + +The Skunkworks - IaC Kickstarter is a template that can be used to give any new teams a functioning IaC deployment pipeline and repository structure. + +> Detailed instructions of CloudBuild trigger setup can be found at [README](./../README.md). diff --git a/examples/guardrails/cloudbuild/skunkworks/main.tf b/examples/guardrails/cloudbuild/skunkworks/main.tf new file mode 100644 index 0000000..03926bd --- /dev/null +++ b/examples/guardrails/cloudbuild/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/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/cloudbuild/skunkworks/variables.tf b/examples/guardrails/cloudbuild/skunkworks/variables.tf new file mode 100644 index 0000000..6af98ff --- /dev/null +++ b/examples/guardrails/cloudbuild/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/github/README.md b/examples/guardrails/github/README.md new file mode 100644 index 0000000..5e051b4 --- /dev/null +++ b/examples/guardrails/github/README.md @@ -0,0 +1,27 @@ +# 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: + +Github + +- 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 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 + +# Getting started + +Deployment and configuration information can be found on the following pages: + +- [Folder Factory](folder-factory) +- [Project Factory](project-factory) +- [Skunkworks - IaC Kickstarter](skunkworks) 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/folder-factory/README.md b/examples/guardrails/github/folder-factory/README.md similarity index 85% rename from examples/guardrails/folder-factory/README.md rename to examples/guardrails/github/folder-factory/README.md index a139948..ee364f8 100644 --- a/examples/guardrails/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. @@ -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). 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/project-factory/README.md b/examples/guardrails/github/project-factory/README.md similarity index 84% rename from examples/guardrails/project-factory/README.md rename to examples/guardrails/github/project-factory/README.md index f901e7f..026fd2e 100644 --- a/examples/guardrails/project-factory/README.md +++ b/examples/guardrails/github/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/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 -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. 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/project-factory/variables.tf b/examples/guardrails/github/project-factory/variables.tf similarity index 100% rename from examples/guardrails/project-factory/variables.tf rename to examples/guardrails/github/project-factory/variables.tf diff --git a/examples/guardrails/project-factory/wif.tf b/examples/guardrails/github/project-factory/wif.tf similarity index 100% rename from examples/guardrails/project-factory/wif.tf rename to examples/guardrails/github/project-factory/wif.tf 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/skunkworks/README.md b/examples/guardrails/github/skunkworks/README.md similarity index 79% rename from examples/guardrails/skunkworks/README.md rename to examples/guardrails/github/skunkworks/README.md index 0b89bb2..158532d 100644 --- a/examples/guardrails/skunkworks/README.md +++ b/examples/guardrails/github/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: + +![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. 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/README.md b/examples/guardrails/gitlab/README.md new file mode 100644 index 0000000..93c4948 --- /dev/null +++ b/examples/guardrails/gitlab/README.md @@ -0,0 +1,29 @@ + +# 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. + +> A more comprehensive description of DevOps & GitOps principles can be found at [DevOps README](./../../../README.md). + +## 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. + +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. + +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/) + + +## 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](./folder-factory) + + + 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..cf6bb06 --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/.gitlab/workflows/.gitlab-ci.yml @@ -0,0 +1,169 @@ +#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 + 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 + - 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. + +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: + 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..c5ffcaa --- /dev/null +++ b/examples/guardrails/gitlab/folder-factory/README.md @@ -0,0 +1,102 @@ +# 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 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. + + +## 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/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 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. + +### 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. 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_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. + + +* .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-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 + * 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: 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/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..f21d44e --- /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..cf6bb06 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/.gitlab/workflows/.gitlab-ci.yml @@ -0,0 +1,169 @@ +#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 + 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 + - 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. + +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: + 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..8b13789 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/.gitlab/workflows/README.md @@ -0,0 +1 @@ + diff --git a/examples/guardrails/gitlab/project-factory/README.md b/examples/guardrails/gitlab/project-factory/README.md new file mode 100644 index 0000000..eaada85 --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/README.md @@ -0,0 +1,109 @@ +# 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/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. + +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. + + +## 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 +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 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. + +### 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. 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_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, 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-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 +* 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: 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/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..56414b9 --- /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..6e51671 --- /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..0e17ef9 --- /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/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 = "" diff --git a/examples/guardrails/gitlab/project-factory/variables.tf b/examples/guardrails/gitlab/project-factory/variables.tf new file mode 100644 index 0000000..c9805dc --- /dev/null +++ b/examples/guardrails/gitlab/project-factory/variables.tf @@ -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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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" { + type = string + description = "GCP Billing Account" +} diff --git a/examples/guardrails/gitlab/project-factory/wif.tf b/examples/guardrails/gitlab/project-factory/wif.tf new file mode 100644 index 0000000..ecd842d --- /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 = var.billing_account +} + +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..17e0389 --- /dev/null +++ b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/.gitlab-ci.yml @@ -0,0 +1,49 @@ +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 + TERRAFORM_POLICY_VALIDATE: $TERRAFORM_POLICY_VALIDATE + POLICY_LIBRARY_REPO : $POLICY_LIBRARY_REPO + + +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..8b13789 --- /dev/null +++ b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/README.md @@ -0,0 +1 @@ + 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..c5936ad --- /dev/null +++ b/examples/guardrails/gitlab/skunkworks/.gitlab/workflows/workflow.yml @@ -0,0 +1,149 @@ +# 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 + - policy-validate + - 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. + +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: + 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..238bf01 --- /dev/null +++ b/examples/guardrails/gitlab/skunkworks/README.md @@ -0,0 +1,64 @@ +# Skunkworks - IaC Kickstarter Template + +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: + +![Gitlab](https://user-images.githubusercontent.com/94000358/224205000-7cfb0fe0-6520-421b-88bd-ba7efb20ffd4.png) + +This template creates a bucket in the specified target environment. + +## 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 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 + +### 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 | +| 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. 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_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-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 +* 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: 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/main.tf b/examples/guardrails/gitlab/skunkworks/main.tf new file mode 100644 index 0000000..cfc6436 --- /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..ab4546b --- /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/README.md b/examples/guardrails/jenkins/README.md new file mode 100644 index 0000000..b539611 --- /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 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 + +# 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 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..f69e778 --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/Jenkinsfile @@ -0,0 +1,81 @@ +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') + policy_file_path = credentials('policy_file_path') + } + 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 Plan') { + steps { + + sh ''' + cd guardrails/folder-factory + terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=${REPO_FULL_NAME}" + 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 new file mode 100644 index 0000000..43dd0b0 --- /dev/null +++ b/examples/guardrails/jenkins/folder-factory/README.md @@ -0,0 +1,100 @@ +# 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). + +## 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/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..5543a60 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/Jenkinsfile @@ -0,0 +1,49 @@ +pipeline { + agent any + environment { + BUCKET_PATH = credentials('backend-path') + policy_file_path = credentials('policy_file_path') + } + options { + skipDefaultCheckout(true) + } + stages { + stage('clean workspace') { + steps { + cleanWs() + } + } + stage('checkout') { + steps { + checkout scm + } + } + 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" -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 new file mode 100644 index 0000000..bd5b101 --- /dev/null +++ b/examples/guardrails/jenkins/project-factory/README.md @@ -0,0 +1,117 @@ +# 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). + +## 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/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..e2856a0 --- /dev/null +++ b/examples/guardrails/jenkins/skunkworks/Jenkinsfile @@ -0,0 +1,78 @@ +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') + 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) + } + 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/${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", + "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 Plan') { + steps { + sh ''' + cd guardrails/skunkworks + terraform init -backend-config="bucket=${BUCKET_PATH}" -backend-config="prefix=prod-skunkworks" + 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 new file mode 100644 index 0000000..7602c43 --- /dev/null +++ b/examples/guardrails/jenkins/skunkworks/README.md @@ -0,0 +1,69 @@ +# 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. +``` +## 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/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/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 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..5087f50 --- /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/folders). 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 0000000..d4e6f82 Binary files /dev/null and b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/diagram.png differ diff --git a/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/README.md b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/README.md new file mode 100644 index 0000000..35198e8 --- /dev/null +++ b/examples/guardrails/terraform-cloud/folder-factory/terraform-cloud-wif/gcp-workload-identity-provider/README.md @@ -0,0 +1,33 @@ +# GCP Workload Identity Provider for Terraform Enterprise + +This terraform code is a part of [GCP Workload Identity Federation for Terraform Enterprise](../) blueprint. + +The codebase provisions the following list of resources: + +- GCS Bucket + + +## 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 +} 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