Merge branch 'master' into psc_and_rlbproxy_subnets

This commit is contained in:
Aleksandr Averbukh 2022-04-13 17:21:00 +02:00 committed by GitHub
commit 2e207eb3a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 488 additions and 184 deletions

View File

@ -1,55 +0,0 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: "FAST Tests"
on:
pull_request:
branches:
- fast-dev
- fast-dev-gke
- master
# paths:
# - 'modules/**'
# - 'fast/stages/**'
# - 'tests/fast/**'
tags:
- ci
- test
env:
TF_PLUGIN_CACHE_DIR: "/home/runner/.terraform.d/plugin-cache"
GOOGLE_APPLICATION_CREDENTIALS: "/home/runner/credentials.json"
PYTEST_ADDOPTS: "--color=yes"
jobs:
all-fast-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Config auth
run: |
echo '{"type": "service_account", "project_id": "test-only"}' \
| tee -a $GOOGLE_APPLICATION_CREDENTIALS
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.9"
- name: Run tests on FAST stages
run: |
mkdir -p ${{ env.TF_PLUGIN_CACHE_DIR }}
pip install -r tests/requirements.txt
pytest -vv tests/fast

View File

@ -26,9 +26,11 @@ on:
- test
env:
TF_PLUGIN_CACHE_DIR: "/home/runner/.terraform.d/plugin-cache"
GOOGLE_APPLICATION_CREDENTIALS: "/home/runner/credentials.json"
PYTEST_ADDOPTS: "--color=yes"
PYTHON_VERSION: 3.9
TF_PLUGIN_CACHE_DIR: "/home/runner/.terraform.d/plugin-cache"
TF_VERSION: 1.1.8
jobs:
doc-examples:
@ -44,9 +46,10 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.9"
python-version: ${{ env.PYTHON_VERSION }}
- name: Run tests on documentation examples
id: pytest
run: |
mkdir -p ${{ env.TF_PLUGIN_CACHE_DIR }}
pip install -r tests/requirements.txt
@ -65,15 +68,16 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.9"
python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Terraform
uses: hashicorp/setup-terraform@v1
with:
terraform_version: 1.1.4
terraform_version: ${{ env.TF_VERSION }}
terraform_wrapper: false
- name: Run tests environments
id: pytest
run: |
mkdir -p ${{ env.TF_PLUGIN_CACHE_DIR }}
pip install -r tests/requirements.txt
@ -92,16 +96,45 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.9"
python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Terraform
uses: hashicorp/setup-terraform@v1
with:
terraform_version: 1.1.4
terraform_version: ${{ env.TF_VERSION }}
terraform_wrapper: false
- name: Run tests modules
id: pytest
run: |
mkdir -p ${{ env.TF_PLUGIN_CACHE_DIR }}
pip install -r tests/requirements.txt
pytest -vv tests/modules
fast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Config auth
run: |
echo '{"type": "service_account", "project_id": "test-only"}' \
| tee -a $GOOGLE_APPLICATION_CREDENTIALS
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Terraform
uses: hashicorp/setup-terraform@v1
with:
terraform_version: ${{ env.TF_VERSION }}
terraform_wrapper: false
- name: Run tests on FAST stages
id: pytest
run: |
mkdir -p ${{ env.TF_PLUGIN_CACHE_DIR }}
pip install -r tests/requirements.txt
pytest -vv tests/fast

1
.gitignore vendored
View File

@ -27,3 +27,4 @@ fast/stages/**/terraform.tfvars.json
fast/stages/**/terraform-*.auto.tfvars.json
fast/stages/**/0*.auto.tfvars*
**/node_modules
fast/stages/**/globals.auto.tfvars.json

View File

@ -18,13 +18,14 @@ from code import interact
import os
from pickletools import int4
import time
import http
import yaml
from collections import defaultdict
from google.api import metric_pb2 as ga_metric
from google.api_core import protobuf_helpers
from google.api_core import exceptions, protobuf_helpers
from google.cloud import monitoring_v3, asset_v1
from google.protobuf import field_mask_pb2
from googleapiclient import discovery
from googleapiclient import discovery, errors
# Organization ID containing the projects to be monitored
ORGANIZATION_ID = os.environ.get("ORGANIZATION_ID")
@ -66,6 +67,9 @@ def main(event, context):
get_l4_forwarding_rules_data(
metrics_dict, l4_forwarding_rules_dict,
limits_dict['internal_forwarding_rules_l4_limit'])
get_l7_forwarding_rules_data(
metrics_dict, l7_forwarding_rules_dict,
limits_dict['internal_forwarding_rules_l7_limit'])
get_vpc_peering_data(metrics_dict,
limits_dict['number_of_vpc_peerings_limit'])
dynamic_routes_dict = get_dynamic_routes(
@ -366,6 +370,11 @@ def get_gce_instances_data(metrics_dict, gce_instance_dict, limit_dict):
current_quota_limit = get_quota_current_limit(f"projects/{project}",
metric_instances_limit)
if current_quota_limit is None:
print(
f"Could not write number of instances for projects/{project} due to missing quotas"
)
current_quota_limit_view = customize_quota_view(current_quota_limit)
for net in network_dict:
@ -519,6 +528,12 @@ def get_l4_forwarding_rules_data(metrics_dict, forwarding_rules_dict,
current_quota_limit = get_quota_current_limit(
f"projects/{project}", L4_FORWARDING_RULES_LIMIT_METRIC)
if current_quota_limit is None:
print(
f"Could not write L4 forwarding rules to metric for projects/{project} due to missing quotas"
)
continue
current_quota_limit_view = customize_quota_view(current_quota_limit)
for net in network_dict:
@ -545,6 +560,55 @@ def get_l4_forwarding_rules_data(metrics_dict, forwarding_rules_dict,
f"Wrote number of L4 forwarding rules to metric for projects/{project}")
def get_l7_forwarding_rules_data(metrics_dict, forwarding_rules_dict,
limit_dict):
'''
Gets the data for L7 Internal Forwarding Rules per VPC Network and writes it to the metric defined in forwarding_rules_metric.
Parameters:
metrics_dict (dictionary of dictionary of string: string): metrics names and descriptions.
forwarding_rules_dict (dictionary of string: int): Keys are the network links and values are the number of Forwarding Rules per network.
limit_dict (dictionary of string:int): Dictionary with the network link as key and the limit as value.
Returns:
None
'''
for project in MONITORED_PROJECTS_LIST:
network_dict = get_networks(project)
current_quota_limit = get_quota_current_limit(
f"projects/{project}", L7_FORWARDING_RULES_LIMIT_METRIC)
if current_quota_limit is None:
print(
f"Could not write number of L7 forwarding rules to metric for projects/{project} due to missing quotas"
)
continue
current_quota_limit_view = customize_quota_view(current_quota_limit)
for net in network_dict:
set_limits(net, current_quota_limit_view, limit_dict)
usage = 0
if net['self_link'] in forwarding_rules_dict:
usage = forwarding_rules_dict[net['self_link']]
write_data_to_metric(
project, usage, metrics_dict["metrics_per_network"]
["l7_forwarding_rules_per_network"]["usage"]["name"],
net['network_name'])
write_data_to_metric(
project, net['limit'], metrics_dict["metrics_per_network"]
["l7_forwarding_rules_per_network"]["limit"]["name"],
net['network_name'])
write_data_to_metric(
project, usage / net['limit'], metrics_dict["metrics_per_network"]
["l7_forwarding_rules_per_network"]["utilization"]["name"],
net['network_name'])
print(
f"Wrote number of L7 forwarding rules to metric for projects/{project}")
def get_pgg_data(metric_dict, usage_dict, limit_metric, limit_dict):
'''
This function gets the usage, limit and utilization per VPC peering group for a specific metric for all projects to be monitored.
@ -564,14 +628,25 @@ def get_pgg_data(metric_dict, usage_dict, limit_metric, limit_dict):
# project_id, network_name, network_id, usage, limit, peerings (list of peered networks)
# peerings is a list of dictionary (one for each peered network) and contains:
# project_id, network_name, network_id
current_quota_limit = get_quota_current_limit(f"projects/{project}",
limit_metric)
if current_quota_limit is None:
print(
f"Could not write number of L7 forwarding rules to metric for projects/{project} due to missing quotas"
)
continue
current_quota_limit_view = customize_quota_view(current_quota_limit)
# For each network in this GCP project
for network_dict in network_dict_list:
if network_dict['network_id'] == 0:
print(
f"Could not write {metric_dict['usage']['name']} for peering group {network_dict['network_name']} in {project} due to missing permissions."
)
continue
network_link = f"https://www.googleapis.com/compute/v1/projects/{project}/global/networks/{network_dict['network_name']}"
current_quota_limit = get_quota_current_limit(f"projects/{project}",
limit_metric)
current_quota_limit_view = customize_quota_view(current_quota_limit)
limit = get_limit_network(network_dict, network_link,
current_quota_limit_view, limit_dict)
@ -590,9 +665,15 @@ def get_pgg_data(metric_dict, usage_dict, limit_metric, limit_dict):
if peered_network_link in usage_dict:
peered_usage = usage_dict[peered_network_link]
peering_project_limit = customize_quota_view(
get_quota_current_limit(
f"projects/{peered_network_dict['project_id']}", limit_metric))
current_peered_quota_limit = get_quota_current_limit(
f"projects/{peered_network_dict['project_id']}", limit_metric)
if current_peered_quota_limit is None:
print(
f"Could not write metrics for peering to projects/{peered_network_dict['project_id']} due to missing quotas"
)
continue
peering_project_limit = customize_quota_view(current_peered_quota_limit)
peered_limit = get_limit_network(peered_network_dict,
peered_network_link,
@ -681,6 +762,11 @@ def count_effective_limit(project_id, network_dict, usage_metric_name,
# Get usage: Sums usage for current network + all peered networks
peering_group_usage = network_dict['usage']
for peered_network in network_dict['peerings']:
if 'usage' not in peered_network:
print(
f"Can not add metrics for peered network in projects/{project_id} as no usage metrics exist due to missing permissions"
)
continue
peering_group_usage += peered_network['usage']
network_link = f"https://www.googleapis.com/compute/v1/projects/{project_id}/global/networks/{network_dict['network_name']}"
@ -694,12 +780,21 @@ def count_effective_limit(project_id, network_dict, usage_metric_name,
for peered_network in network_dict['peerings']:
peered_network_link = f"https://www.googleapis.com/compute/v1/projects/{peered_network['project_id']}/global/networks/{peered_network['network_name']}"
limit_step2.append(
max(peered_network['limit'],
get_limit_ppg(peered_network_link, limit_dict)))
if 'limit' in peered_network:
limit_step2.append(
max(peered_network['limit'],
get_limit_ppg(peered_network_link, limit_dict)))
else:
print(
f"Ignoring projects/{peered_network['project_id']} for limits in peering group of project {project_id} as no limits are available."
+
"This can happen if you don't have permissions on the project, for example if the project is in another organization or a Google managed project"
)
# Calculates effective limit: Step 3: Find minimum from the list created by Step 2
limit_step3 = min(limit_step2)
limit_step3 = 0
if len(limit_step2) > 0:
limit_step3 = min(limit_step2)
# Calculates effective limit: Step 4: Find maximum from step 1 and step 3
effective_limit = max(limit_step1, limit_step3)
@ -837,8 +932,9 @@ def get_routes_for_router(project_id, router_region, router_name):
sum_routes = 0
if 'result' in response:
for peer in response['result']['bgpPeerStatus']:
sum_routes += peer['numLearnedRoutes']
if 'bgpPeerStatus' in response['result']:
for peer in response['result']['bgpPeerStatus']:
sum_routes += peer['numLearnedRoutes']
return sum_routes
@ -939,7 +1035,18 @@ def get_network_id(project_id, network_name):
network_id (int): Network ID.
'''
request = service.networks().list(project=project_id)
response = request.execute()
try:
response = request.execute()
except errors.HttpError as err:
# TODO: log proper warning
if err.resp.status == http.HTTPStatus.FORBIDDEN:
print(
f"Warning: error reading networks for {project_id}. " +
f"This can happen if you don't have permissions on the project, for example if the project is in another organization or a Google managed project"
)
else:
print(f"Warning: error reading networks for {project_id}: {err}")
return 0
network_id = 0
@ -967,15 +1074,22 @@ def get_quota_current_limit(project_link, metric_name):
'''
client, interval = create_client()
results = client.list_time_series(
request={
"name": project_link,
"filter": f'metric.type = "{metric_name}"',
"interval": interval,
"view": monitoring_v3.ListTimeSeriesRequest.TimeSeriesView.FULL
})
results_list = list(results)
return results_list
try:
results = client.list_time_series(
request={
"name": project_link,
"filter": f'metric.type = "{metric_name}"',
"interval": interval,
"view": monitoring_v3.ListTimeSeriesRequest.TimeSeriesView.FULL
})
results_list = list(results)
return results_list
except exceptions.PermissionDenied as err:
print(
f"Warning: error reading quotas for {project_link}. " +
f"This can happen if you don't have permissions on the project, for example if the project is in another organization or a Google managed project"
)
return None
def customize_quota_view(quota_results):

View File

@ -15,8 +15,9 @@
*/
locals {
project_id_list = toset(var.monitored_projects_list)
projects = join(",", local.project_id_list)
project_id_list = toset(var.monitored_projects_list)
projects = join(",", local.project_id_list)
monitoring_project = var.monitoring_project_id == "" ? module.project-monitoring[0].project_id : var.monitoring_project_id
}
################################################
@ -24,6 +25,7 @@ locals {
################################################
module "project-monitoring" {
count = var.monitoring_project_id == "" ? 1 : 0
source = "../../../modules/project"
name = "monitoring"
parent = "organizations/${var.organization_id}"
@ -38,7 +40,7 @@ module "project-monitoring" {
module "service-account-function" {
source = "../../../modules/iam-service-account"
project_id = module.project-monitoring.project_id
project_id = local.monitoring_project
name = "sa-dash"
generate_key = false
@ -54,7 +56,7 @@ module "service-account-function" {
}
iam_project_roles = {
"${module.project-monitoring.project_id}" = [
"${local.monitoring_project}" = [
"roles/monitoring.metricWriter"
]
}
@ -66,7 +68,7 @@ module "service-account-function" {
module "pubsub" {
source = "../../../modules/pubsub"
project_id = module.project-monitoring.project_id
project_id = local.monitoring_project
name = "network-dashboard-pubsub"
subscriptions = {
"network-dashboard-pubsub-default" = null
@ -76,7 +78,7 @@ module "pubsub" {
}
resource "google_cloud_scheduler_job" "job" {
project = module.project-monitoring.project_id
project = local.monitoring_project
region = var.region
name = "network-dashboard-scheduler"
schedule = var.schedule_cron
@ -90,9 +92,9 @@ resource "google_cloud_scheduler_job" "job" {
module "cloud-function" {
source = "../../../modules/cloud-function"
project_id = module.project-monitoring.project_id
project_id = local.monitoring_project
name = "network-dashboard-cloud-function"
bucket_name = "network-dashboard-bucket"
bucket_name = "${local.monitoring_project}-network-dashboard-bucket"
bucket_config = {
location = var.region
lifecycle_delete_age = null
@ -114,7 +116,7 @@ module "cloud-function" {
environment_variables = {
MONITORED_PROJECTS_LIST = local.projects
MONITORING_PROJECT_ID = module.project-monitoring.project_id
MONITORING_PROJECT_ID = local.monitoring_project
ORGANIZATION_ID = var.organization_id
}
@ -133,5 +135,5 @@ module "cloud-function" {
resource "google_monitoring_dashboard" "dashboard" {
dashboard_json = file("${path.module}/dashboards/quotas-utilization.json")
project = module.project-monitoring.project_id
project = local.monitoring_project
}

View File

@ -22,8 +22,14 @@ variable "billing_account" {
description = "The ID of the billing account to associate this project with"
}
variable "monitoring_project_id" {
description = "Monitoring project where the dashboard will be created and the solution deployed; a project will be created if set to empty string"
default = ""
}
variable "prefix" {
description = "Customer name to use as prefix for resources' naming"
description = "Customer name to use as prefix for monitoring project"
default = ""
}
# TODO: support folder instead of a list of projects?
@ -38,8 +44,9 @@ variable "schedule_cron" {
}
variable "project_monitoring_services" {
description = "Service APIs enabled by default in new projects."
description = "Service APIs enabled in the monitoring project if it will be created."
default = [
"cloudasset.googleapis.com",
"cloudbilling.googleapis.com",
"cloudbuild.googleapis.com",
"cloudresourcemanager.googleapis.com",
@ -50,29 +57,11 @@ variable "project_monitoring_services" {
"iamcredentials.googleapis.com",
"logging.googleapis.com",
"monitoring.googleapis.com",
"oslogin.googleapis.com",
"servicenetworking.googleapis.com",
"serviceusage.googleapis.com",
]
}
variable "project_vm_services" {
description = "Service APIs enabled by default in new projects."
default = [
"cloudbilling.googleapis.com",
"compute.googleapis.com",
"logging.googleapis.com",
"monitoring.googleapis.com",
"servicenetworking.googleapis.com",
]
}
variable "region" {
description = "Region used to deploy subnets"
description = "Region used to deploy the cloud functions and scheduler"
default = "europe-west1"
}
variable "zone" {
description = "Zone used to deploy vms"
default = "europe-west1-b"
}

View File

@ -183,20 +183,21 @@ Due to its simplicity, this stage lends itself easily to customizations: adding
| [groups](variables.tf#L118) | Group names to grant organization-level permissions. | <code>map&#40;string&#41;</code> | | <code title="&#123;&#10; gcp-billing-admins &#61; &#34;gcp-billing-admins&#34;,&#10; gcp-devops &#61; &#34;gcp-devops&#34;,&#10; gcp-network-admins &#61; &#34;gcp-network-admins&#34;&#10; gcp-organization-admins &#61; &#34;gcp-organization-admins&#34;&#10; gcp-security-admins &#61; &#34;gcp-security-admins&#34;&#10; gcp-support &#61; &#34;gcp-support&#34;&#10;&#125;">&#123;&#8230;&#125;</code> | <code>00-bootstrap</code> |
| [organization_policy_configs](variables.tf#L143) | Organization policies customization. | <code title="object&#40;&#123;&#10; allowed_policy_member_domains &#61; list&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> | |
| [outputs_location](variables.tf#L151) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable | <code>string</code> | | <code>null</code> | |
| [team_folders](variables.tf#L168) | Team folders to be created. Format is described in a code comment. | <code title="map&#40;object&#40;&#123;&#10; descriptive_name &#61; string&#10; group_iam &#61; map&#40;list&#40;string&#41;&#41;&#10; impersonation_groups &#61; list&#40;string&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>null</code> | |
| [tag_names](variables.tf#L168) | Customized names for resource management tags. | <code title="object&#40;&#123;&#10; context &#61; string&#10; environment &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; context &#61; &#34;context&#34;&#10; environment &#61; &#34;environment&#34;&#10;&#125;">&#123;&#8230;&#125;</code> | |
| [team_folders](variables.tf#L185) | Team folders to be created. Format is described in a code comment. | <code title="map&#40;object&#40;&#123;&#10; descriptive_name &#61; string&#10; group_iam &#61; map&#40;list&#40;string&#41;&#41;&#10; impersonation_groups &#61; list&#40;string&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>null</code> | |
## Outputs
| name | description | sensitive | consumers |
|---|---|:---:|---|
| [cicd_repositories](outputs.tf#L156) | WIF configuration for CI/CD repositories. | | |
| [dataplatform](outputs.tf#L168) | Data for the Data Platform stage. | | |
| [networking](outputs.tf#L184) | Data for the networking stage. | | |
| [project_factories](outputs.tf#L193) | Data for the project factories stage. | | |
| [providers](outputs.tf#L209) | Terraform provider files for this stage and dependent stages. | ✓ | <code>02-networking</code> · <code>02-security</code> · <code>03-dataplatform</code> · <code>xx-sandbox</code> · <code>xx-teams</code> |
| [sandbox](outputs.tf#L216) | Data for the sandbox stage. | | <code>xx-sandbox</code> |
| [security](outputs.tf#L226) | Data for the networking stage. | | <code>02-security</code> |
| [teams](outputs.tf#L236) | Data for the teams stage. | | |
| [tfvars](outputs.tf#L249) | Terraform variable files for the following stages. | ✓ | |
| [cicd_repositories](outputs.tf#L157) | WIF configuration for CI/CD repositories. | | |
| [dataplatform](outputs.tf#L169) | Data for the Data Platform stage. | | |
| [networking](outputs.tf#L185) | Data for the networking stage. | | |
| [project_factories](outputs.tf#L194) | Data for the project factories stage. | | |
| [providers](outputs.tf#L210) | Terraform provider files for this stage and dependent stages. | ✓ | <code>02-networking</code> · <code>02-security</code> · <code>03-dataplatform</code> · <code>xx-sandbox</code> · <code>xx-teams</code> |
| [sandbox](outputs.tf#L217) | Data for the sandbox stage. | | <code>xx-sandbox</code> |
| [security](outputs.tf#L227) | Data for the networking stage. | | <code>02-security</code> |
| [teams](outputs.tf#L237) | Data for the teams stage. | | |
| [tfvars](outputs.tf#L250) | Terraform variable files for the following stages. | ✓ | |
<!-- END TFDOC -->

View File

@ -21,7 +21,9 @@ module "branch-dp-folder" {
parent = "organizations/${var.organization.id}"
name = "Data Platform"
tag_bindings = {
context = try(module.organization.tag_values["context/data"].id, null)
context = try(
module.organization.tag_values["${var.tag_names.context}/data"].id, null
)
}
}
@ -39,7 +41,9 @@ module "branch-dp-dev-folder" {
"roles/resourcemanager.projectCreator" = [module.branch-dp-dev-sa.iam_email]
}
tag_bindings = {
context = try(module.organization.tag_values["environment/development"].id, null)
context = try(
module.organization.tag_values["${var.tag_names.environment}/development"].id, null
)
}
}
@ -57,7 +61,9 @@ module "branch-dp-prod-folder" {
"roles/resourcemanager.projectCreator" = [module.branch-dp-prod-sa.iam_email]
}
tag_bindings = {
context = try(module.organization.tag_values["environment/production"].id, null)
context = try(
module.organization.tag_values["${var.tag_names.environment}/production"].id, null
)
}
}

View File

@ -39,7 +39,9 @@ module "branch-network-folder" {
"roles/compute.xpnAdmin" = [module.branch-network-sa.iam_email]
}
tag_bindings = {
context = try(module.organization.tag_values["context/networking"].id, null)
context = try(
module.organization.tag_values["${var.tag_names.context}/networking"].id, null
)
}
}
@ -54,7 +56,9 @@ module "branch-network-prod-folder" {
]
}
tag_bindings = {
environment = try(module.organization.tag_values["environment/production"].id, null)
environment = try(
module.organization.tag_values["${var.tag_names.environment}/production"].id, null
)
}
}
@ -69,7 +73,9 @@ module "branch-network-dev-folder" {
]
}
tag_bindings = {
environment = try(module.organization.tag_values["environment/development"].id, null)
environment = try(
module.organization.tag_values["${var.tag_names.environment}/development"].id, null
)
}
}

View File

@ -38,7 +38,9 @@ module "branch-sandbox-folder" {
}
}
tag_bindings = {
context = try(module.organization.tag_values["context/sandbox"].id, null)
context = try(
module.organization.tag_values["${var.tag_names.context}/sandbox"].id, null
)
}
}

View File

@ -40,7 +40,9 @@ module "branch-security-folder" {
"roles/resourcemanager.projectCreator" = [module.branch-security-sa.iam_email]
}
tag_bindings = {
context = try(module.organization.tag_values["context/security"].id, null)
context = try(
module.organization.tag_values["${var.tag_names.context}/security"].id, null
)
}
}

View File

@ -21,7 +21,9 @@ module "branch-teams-folder" {
parent = "organizations/${var.organization.id}"
name = "Teams"
tag_bindings = {
context = try(module.organization.tag_values["context/teams"].id, null)
context = try(
module.organization.tag_values["${var.tag_names.context}/teams"].id, null
)
}
}
@ -90,7 +92,9 @@ module "branch-teams-team-dev-folder" {
"roles/resourcemanager.projectCreator" = [module.branch-teams-dev-pf-sa.iam_email]
}
tag_bindings = {
environment = try(module.organization.tag_values["environment/development"].id, null)
environment = try(
module.organization.tag_values["${var.tag_names.environment}/development"].id, null
)
}
}
@ -111,7 +115,9 @@ module "branch-teams-team-prod-folder" {
"roles/resourcemanager.projectCreator" = [module.branch-teams-prod-pf-sa.iam_email]
}
tag_bindings = {
environment = try(module.organization.tag_values["environment/production"].id, null)
environment = try(
module.organization.tag_values["${var.tag_names.environment}/production"].id, null
)
}
}

View File

@ -1 +0,0 @@
/home/ludomagno/Desktop/dev/tf-playground/config/tfvars/globals.auto.tfvars.json

View File

@ -151,7 +151,7 @@ module "organization" {
# )
}
tags = {
context = {
(var.tag_names.context) = {
description = "Resource management context."
iam = {}
values = {
@ -163,7 +163,7 @@ module "organization" {
teams = null
}
}
environment = {
(var.tag_names.environment) = {
description = "Environment definition."
iam = {}
values = {
@ -190,9 +190,9 @@ resource "google_organization_iam_member" "org_policy_admin" {
title = "org_policy_tag_scoped"
description = "Org policy tag scoped grant for ${each.value.0}/${each.value.1}."
expression = <<-END
resource.matchTag('${var.organization.id}/context', '${each.value.0}')
resource.matchTag('${var.organization.id}/${var.tag_names.context}', '${each.value.0}')
&&
resource.matchTag('${var.organization.id}/environment', '${each.value.1}')
resource.matchTag('${var.organization.id}/${var.tag_names.environment}', '${each.value.1}')
END
}
}

View File

@ -150,6 +150,7 @@ locals {
tfvars = {
folder_ids = local.folder_ids
service_accounts = local.service_accounts
tag_names = var.tag_names
}
}

View File

@ -165,6 +165,23 @@ variable "prefix" {
}
}
variable "tag_names" {
description = "Customized names for resource management tags."
type = object({
context = string
environment = string
})
default = {
context = "context"
environment = "environment"
}
nullable = false
validation {
condition = alltrue([for k, v in var.tag_names : v != null])
error_message = "Tag names cannot be null."
}
}
variable "team_folders" {
description = "Team folders to be created. Format is described in a code comment."
type = map(object({

View File

@ -56,8 +56,8 @@ module "db" {
tier = "db-g1-small"
replicas = {
replica1 = "europe-west3"
replica2 = "us-central1"
replica1 = { region = "europe-west3", encryption_key_name = null }
replica2 = { region = "us-central1", encryption_key_name = null }
}
}
# tftest modules=1 resources=3
@ -93,6 +93,53 @@ module "db" {
}
# tftest modules=1 resources=6
```
### CMEK encryption
```hcl
module "project" {
source = "./modules/project"
billing_account = var.billing_account_id
parent = var.organization_id
name = "my-db-project"
services = [
"servicenetworking.googleapis.com",
"sqladmin.googleapis.com",
]
}
module "kms" {
source = "./modules/kms"
project_id = module.project.project_id
keyring = {
name = "keyring"
location = var.region
}
keys = {
key-sql = null
}
key_iam = {
key-sql = {
"roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
"serviceAccount:${module.project.service_accounts.robots.sqladmin}"
]
}
}
}
module "db" {
source = "./modules/cloudsql-instance"
project_id = module.project.project_id
encryption_key_name = module.kms.keys["key-sql"].id
network = var.vpc.self_link
name = "db"
region = var.region
database_version = "POSTGRES_13"
tier = "db-g1-small"
}
# tftest modules=3 resources=10
```
<!-- BEGIN TFDOC -->
## Variables
@ -100,11 +147,11 @@ module "db" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [database_version](variables.tf#L50) | Database type and version to create. | <code>string</code> | ✓ | |
| [name](variables.tf#L91) | Name of primary replica. | <code>string</code> | ✓ | |
| [network](variables.tf#L96) | VPC self link where the instances will be deployed. Private Service Networking must be enabled and configured in this VPC. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L107) | The ID of the project where this instances will be created. | <code>string</code> | ✓ | |
| [region](variables.tf#L112) | Region of the primary replica. | <code>string</code> | ✓ | |
| [tier](variables.tf#L123) | The machine type to use for the instances. | <code>string</code> | ✓ | |
| [name](variables.tf#L97) | Name of primary instance. | <code>string</code> | ✓ | |
| [network](variables.tf#L102) | VPC self link where the instances will be deployed. Private Service Networking must be enabled and configured in this VPC. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L113) | The ID of the project where this instances will be created. | <code>string</code> | ✓ | |
| [region](variables.tf#L118) | Region of the primary instance. | <code>string</code> | ✓ | |
| [tier](variables.tf#L132) | The machine type to use for the instances. | <code>string</code> | ✓ | |
| [authorized_networks](variables.tf#L17) | Map of NAME=>CIDR_RANGE to allow to connect to the database(s). | <code>map&#40;string&#41;</code> | | <code>null</code> |
| [availability_type](variables.tf#L23) | Availability type for the primary replica. Either `ZONAL` or `REGIONAL`. | <code>string</code> | | <code>&#34;ZONAL&#34;</code> |
| [backup_configuration](variables.tf#L29) | Backup settings for primary instance. Will be automatically enabled if using MySQL with one or more replicas. | <code title="object&#40;&#123;&#10; enabled &#61; bool&#10; binary_log_enabled &#61; bool&#10; start_time &#61; string&#10; location &#61; string&#10; log_retention_days &#61; number&#10; retention_count &#61; number&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; enabled &#61; false&#10; binary_log_enabled &#61; false&#10; start_time &#61; &#34;23:00&#34;&#10; location &#61; null&#10; log_retention_days &#61; 7&#10; retention_count &#61; 7&#10;&#125;">&#123;&#8230;&#125;</code> |
@ -112,11 +159,12 @@ module "db" {
| [deletion_protection](variables.tf#L61) | Allow terraform to delete instances. | <code>bool</code> | | <code>false</code> |
| [disk_size](variables.tf#L67) | Disk size in GB. Set to null to enable autoresize. | <code>number</code> | | <code>null</code> |
| [disk_type](variables.tf#L73) | The type of data disk: `PD_SSD` or `PD_HDD`. | <code>string</code> | | <code>&#34;PD_SSD&#34;</code> |
| [flags](variables.tf#L79) | Map FLAG_NAME=>VALUE for database-specific tuning. | <code>map&#40;string&#41;</code> | | <code>null</code> |
| [labels](variables.tf#L85) | Labels to be attached to all instances. | <code>map&#40;string&#41;</code> | | <code>null</code> |
| [prefix](variables.tf#L101) | Prefix used to generate instance names. | <code>string</code> | | <code>null</code> |
| [replicas](variables.tf#L117) | Map of NAME=>REGION for additional read replicas. Set to null to disable replica creation. | <code>map&#40;any&#41;</code> | | <code>null</code> |
| [users](variables.tf#L128) | Map of users to create in the primary instance (and replicated to other replicas) in the format USER=>PASSWORD. For MySQL, anything afterr the first `@` (if persent) will be used as the user's host. Set PASSWORD to null if you want to get an autogenerated password. | <code>map&#40;string&#41;</code> | | <code>null</code> |
| [encryption_key_name](variables.tf#L79) | The full path to the encryption key used for the CMEK disk encryption of the primary instance. | <code>string</code> | | <code>null</code> |
| [flags](variables.tf#L85) | Map FLAG_NAME=>VALUE for database-specific tuning. | <code>map&#40;string&#41;</code> | | <code>null</code> |
| [labels](variables.tf#L91) | Labels to be attached to all instances. | <code>map&#40;string&#41;</code> | | <code>null</code> |
| [prefix](variables.tf#L107) | Prefix used to generate instance names. | <code>string</code> | | <code>null</code> |
| [replicas](variables.tf#L123) | Map of NAME=> {REGION, KMS_KEY} for additional read replicas. Set to null to disable replica creation. | <code title="map&#40;object&#40;&#123;&#10; region &#61; string&#10; encryption_key_name &#61; string&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [users](variables.tf#L137) | Map of users to create in the primary instance (and replicated to other replicas) in the format USER=>PASSWORD. For MySQL, anything afterr the first `@` (if persent) will be used as the user's host. Set PASSWORD to null if you want to get an autogenerated password. | <code>map&#40;string&#41;</code> | | <code>null</code> |
## Outputs
@ -129,8 +177,10 @@ module "db" {
| [instances](outputs.tf#L50) | Cloud SQL instance resources. | ✓ |
| [ip](outputs.tf#L56) | IP address of the primary instance. | |
| [ips](outputs.tf#L61) | IP addresses of all instances. | |
| [self_link](outputs.tf#L69) | Self link of the primary instance. | |
| [self_links](outputs.tf#L74) | Self links of all instances. | |
| [user_passwords](outputs.tf#L82) | Map of containing the password of all users created through terraform. | ✓ |
| [name](outputs.tf#L69) | Name of the primary instance. | |
| [names](outputs.tf#L74) | Names of all instances. | |
| [self_link](outputs.tf#L82) | Self link of the primary instance. | |
| [self_links](outputs.tf#L87) | Self links of all instances. | |
| [user_passwords](outputs.tf#L95) | Map of containing the password of all users created through terraform. | ✓ |
<!-- END TFDOC -->

View File

@ -39,10 +39,12 @@ locals {
}
resource "google_sql_database_instance" "primary" {
project = var.project_id
name = "${local.prefix}${var.name}"
region = var.region
database_version = var.database_version
provider = google-beta
project = var.project_id
name = "${local.prefix}${var.name}"
region = var.region
database_version = var.database_version
encryption_key_name = var.encryption_key_name
settings {
tier = var.tier
@ -99,11 +101,13 @@ resource "google_sql_database_instance" "primary" {
}
resource "google_sql_database_instance" "replicas" {
provider = google-beta
for_each = local.has_replicas ? var.replicas : {}
project = var.project_id
name = "${local.prefix}${each.key}"
region = each.value
region = each.value.region
database_version = var.database_version
encryption_key_name = each.value.encryption_key_name
master_instance_name = google_sql_database_instance.primary.name
settings {

View File

@ -66,6 +66,19 @@ output "ips" {
}
}
output "name" {
description = "Name of the primary instance."
value = google_sql_database_instance.primary.name
}
output "names" {
description = "Names of all instances."
value = {
for id, instance in local._all_intances :
id => instance.name
}
}
output "self_link" {
description = "Self link of the primary instance."
value = google_sql_database_instance.primary.self_link

View File

@ -76,6 +76,12 @@ variable "disk_type" {
default = "PD_SSD"
}
variable "encryption_key_name" {
description = "The full path to the encryption key used for the CMEK disk encryption of the primary instance."
type = string
default = null
}
variable "flags" {
description = "Map FLAG_NAME=>VALUE for database-specific tuning."
type = map(string)
@ -89,7 +95,7 @@ variable "labels" {
}
variable "name" {
description = "Name of primary replica."
description = "Name of primary instance."
type = string
}
@ -110,14 +116,17 @@ variable "project_id" {
}
variable "region" {
description = "Region of the primary replica."
description = "Region of the primary instance."
type = string
}
variable "replicas" {
description = "Map of NAME=>REGION for additional read replicas. Set to null to disable replica creation."
type = map(any)
default = null
description = "Map of NAME=> {REGION, KMS_KEY} for additional read replicas. Set to null to disable replica creation."
type = map(object({
region = string
encryption_key_name = string
}))
default = {}
}
variable "tier" {

View File

@ -1,8 +1,19 @@
# Project Module
## Examples
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.
### Minimal example with IAM
## 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 {
@ -28,16 +39,43 @@ module "project" {
# tftest modules=1 resources=4
```
### Minimal example with IAM additive roles
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 = "./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 = "./modules/project"
name = "project-example"
iam_additive = {
"roles/viewer" = [
"group:one@example.org", "group:two@xample.org"
"group:one@example.org",
"group:two@xample.org"
],
"roles/storage.objectAdmin" = [
"group:two@example.org"
@ -50,13 +88,54 @@ module "project" {
# tftest modules=1 resources=5
```
### Shared VPC service
### 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 = "./modules/project"
name = "project-example"
group_iam = {
"roles/editor" = [
"group:foo@example.com"
]
}
iam = {
"roles/editor" = [
"serviceAccount:${module.project.service_accounts.cloud_services}"
]
}
}
# tftest modules=1 resources=3
```
## 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 = "./modules/project"
name = "project-example"
shared_vpc_host_config = {
enabled = true
service_projects = []
}
}
# tftest modules=1 resources=2
```
### Service project
```hcl
module "project" {
source = "./modules/project"
name = "project-example"
shared_vpc_service_config = {
attach = true
host_project = "my-host-project"
@ -76,7 +155,7 @@ module "project" {
# tftest modules=1 resources=6
```
### Organization policies
## Organization policies
```hcl
module "project" {
@ -184,6 +263,8 @@ module "project-host" {
## Cloud KMS encryption keys
The module offers a simple, centralized way to assign `roles/cloudkms.cryptoKeyEncrypterDecrypter` to service identities.
```hcl
module "project" {
source = "./modules/project"
@ -238,6 +319,27 @@ module "project" {
# 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 = "./modules/project"
name = "project-example"
services = [
"compute.googleapis.com"
]
}
output "compute_robot" {
value = module.project.service_accounts.robots.compute
}
# tftest modules=1 resources=2
```
<!-- TFDOC OPTS files:1 -->
<!-- BEGIN TFDOC -->

View File

@ -42,7 +42,9 @@ locals {
gcf = "service-%s@gcf-admin-robot"
pubsub = "service-%s@gcp-sa-pubsub"
secretmanager = "service-%s@gcp-sa-secretmanager"
sql = "service-%s@gcp-sa-cloud-sql"
storage = "service-%s@gs-project-accounts"
sqladmin = "service-%s@gcp-sa-cloud-sql"
}
service_accounts_default = {
compute = "${local.project.number}-compute@developer.gserviceaccount.com"
@ -56,9 +58,10 @@ locals {
k => "${format(v, local.project.number)}.iam.gserviceaccount.com"
}
service_accounts_jit_services = [
"secretmanager.googleapis.com",
"cloudasset.googleapis.com",
"pubsub.googleapis.com",
"cloudasset.googleapis.com"
"secretmanager.googleapis.com",
"sqladmin.googleapis.com"
]
service_accounts_cmek_service_keys = distinct(flatten([
for s in keys(var.service_encryption_key_ids) : [

View File

@ -94,7 +94,7 @@ variable "region" {
}
variable "replicas" {
type = map(any)
type = any
default = null
}

View File

@ -35,8 +35,8 @@ def test_prefix(plan_runner):
assert r['values']['name'] == 'prefix-db'
replicas = """{
replica1 = "europe-west3"
replica2 = "us-central1"
replica1 = { region = "europe-west3", encryption_key_name = null }
replica2 = { region = "us-central1", encryption_key_name = null }
}"""
_, resources = plan_runner(prefix="prefix")
@ -49,8 +49,8 @@ def test_replicas(plan_runner):
"Test replicated instance."
replicas = """{
replica1 = "europe-west3"
replica2 = "us-central1"
replica1 = { region = "europe-west3", encryption_key_name = null }
replica2 = { region = "us-central1", encryption_key_name = null }
}"""
_, resources = plan_runner(replicas=replicas, prefix="prefix")
@ -80,10 +80,9 @@ def test_mysql_replicas_enables_backup(plan_runner):
"Test MySQL backup setup with replicas."
replicas = """{
replica1 = "europe-west3"
replica1 = { region = "europe-west3", encryption_key_name = null }
}"""
_, resources = plan_runner(replicas=replicas,
database_version="MYSQL_8_0")
_, resources = plan_runner(replicas=replicas, database_version="MYSQL_8_0")
assert len(resources) == 2
primary = [r for r in resources if r['name'] == 'primary'][0]
backup_config = primary['values']['settings'][0]['backup_configuration'][0]