init: repository upload

This commit is contained in:
s.licata 2024-05-20 13:04:04 +02:00
commit ab7d2651ce
533 changed files with 54189 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.vscode
.terraform
*.terraform.*
*.tfstate*
.terraform.lock.hcl
*symlink_provider.tf
.DS_Store
locals/
**/private_keys

View File

@ -0,0 +1,71 @@
##-------------------------------------------------------------------------------
## d4science - NETWORKING Folder
##-------------------------------------------------------------------------------
module "d4science-networking-folder" {
source = "../assets/modules-fabric/v26/folder"
parent = "organizations/${var.organization.id}"
name = "Networking"
folder_create = true
iam = {
"roles/owner" = [
module.d4science-networking-tfsa.iam_email,
"group:foundationreply@d4science.org"
]
"roles/compute.xpnAdmin" = [module.d4science-networking-tfsa.iam_email] #to enable shared VPC
#to enable 01-networking create the hub
"roles/resourcemanager.projectCreator" = [module.d4science-networking-tfsa.iam_email]
}
# iam_additive = {
## "roles/resourcemanager.projectCreator" = [module.common-terraform-sa.iam_email] # required to create project within this folder
# "roles/resourcemanager.projectCreator" = [] # required to create project within this folder
#
# }
}
##-------------------------------------------------------------------------------
## 01 - Networking - TF SA, impersonated to apply Terraform config
##-------------------------------------------------------------------------------
module "d4science-networking-tfsa" {
source = "../assets/modules-fabric/v26/iam-service-account"
project_id = module.d4science-seed-project.project_id
name = "d4science-com-tfnet-sa"
prefix = var.prefix
iam = {
"roles/iam.serviceAccountTokenCreator" = ["group:foundationreply@d4science.org"]
#Impersonate service accounts (create OAuth2 access tokens, sign blobs or JWTs, etc).
}
iam_billing_roles = {
"${var.billing_account_id}" = ["roles/billing.user"]
}
}
#
##-------------------------------------------------------------------------------
## 01 - Networking - TF Bucket, store Terraform state
##-------------------------------------------------------------------------------
module "d4science-networking-tfbucket" {
source = "../assets/modules-fabric/v26/gcs"
name = "d4science-com-ew8-foundation-tfnet-bkt"
project_id = module.d4science-seed-project.project_id
prefix = var.prefix
versioning = true
iam = {
"roles/storage.objectAdmin" = [module.d4science-networking-tfsa.iam_email]
}
location = "EUROPE-WEST8"
storage_class = "STANDARD"
labels = var.labels
}

View File

@ -0,0 +1,42 @@
##-------------------------------------------------------------------------------
## 02 - Security - TF SA, impersonated to apply Terraform config
##-------------------------------------------------------------------------------
module "d4science-security-tfsa" {
source = "../assets/modules-fabric/v26/iam-service-account"
project_id = module.d4science-seed-project.project_id
name = "d4science-com-tfsec-sa"
prefix = var.prefix
iam = {
"roles/iam.serviceAccountTokenCreator" = ["group:foundationreply@d4science.org"]
#Impersonate service accounts (create OAuth2 access tokens, sign blobs or JWTs, etc).
}
iam_organization_roles = {
"${var.organization.id}" = ["roles/resourcemanager.organizationAdmin"]
}
}
#
##-------------------------------------------------------------------------------
## 01 - Networking - TF Bucket, store Terraform state
##-------------------------------------------------------------------------------
module "d4science-security-tfbucket" {
source = "../assets/modules-fabric/v26/gcs"
name = "d4science-com-ew8-foundation-tfsec-bkt"
project_id = module.d4science-seed-project.project_id
prefix = var.prefix
versioning = true
iam = {
"roles/storage.objectAdmin" = [module.d4science-security-tfsa.iam_email]
}
location = "EUROPE-WEST8"
storage_class = "STANDARD"
labels = var.labels
}

View File

@ -0,0 +1,127 @@
##-------------------------------------------------------------------------------
## d4science/Prod Folder
##-------------------------------------------------------------------------------
module "d4science-prod-folder" {
source = "../assets/modules-fabric/v26/folder"
parent = "organizations/${var.organization.id}"
name = "Prod"
folder_create = true
#https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/tree/master/blueprints/factories/project-factory
iam = {
#Required for Project Factory
"roles/logging.admin" = [module.d4science-project-factory-prod-tfsa.iam_email]
"roles/owner" = [module.d4science-project-factory-prod-tfsa.iam_email]
"roles/resourcemanager.folderAdmin" = [module.d4science-project-factory-prod-tfsa.iam_email]
"roles/resourcemanager.projectCreator" = [module.d4science-project-factory-prod-tfsa.iam_email]
#To enable Shared VPC Service projects
"roles/compute.xpnAdmin" = [module.d4science-networking-tfsa.iam_email]
}
}
##-------------------------------------------------------------------------------
## d4science/Test Folder
##-------------------------------------------------------------------------------
module "d4science-test-folder" {
source = "../assets/modules-fabric/v26/folder"
parent = "organizations/${var.organization.id}"
name = "Test"
folder_create = true
#https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/tree/master/blueprints/factories/project-factory
iam = {
#Required for Project Factory
"roles/logging.admin" = [module.d4science-project-factory-test-tfsa.iam_email]
"roles/owner" = [module.d4science-project-factory-test-tfsa.iam_email]
"roles/resourcemanager.folderAdmin" = [module.d4science-project-factory-test-tfsa.iam_email]
"roles/resourcemanager.projectCreator" = [module.d4science-project-factory-test-tfsa.iam_email]
#To enable Shared VPC Service projects
"roles/compute.xpnAdmin" = [module.d4science-networking-tfsa.iam_email]
}
}
##-------------------------------------------------------------------------------
## 03 - Project Factory - TF SA, impersonated to apply Terraform config
##-------------------------------------------------------------------------------
module "d4science-project-factory-test-tfsa" {
source = "../assets/modules-fabric/v26/iam-service-account"
project_id = module.d4science-seed-project.project_id
name = "d4science-test-tfprj-sa"
prefix = var.prefix
iam = {
"roles/iam.serviceAccountTokenCreator" = ["group:foundationreply@d4science.org"]
#Impersonate service accounts (create OAuth2 access tokens, sign blobs or JWTs, etc).
}
iam_billing_roles = {
"${var.billing_account_id}" = ["roles/billing.admin"]
}
}
module "d4science-project-factory-prod-tfsa" {
source = "../assets/modules-fabric/v26/iam-service-account"
project_id = module.d4science-seed-project.project_id
name = "d4science-prod-tfprj-sa"
prefix = var.prefix
iam = {
"roles/iam.serviceAccountTokenCreator" = ["group:foundationreply@d4science.org"]
#Impersonate service accounts (create OAuth2 access tokens, sign blobs or JWTs, etc).
}
iam_billing_roles = {
"${var.billing_account_id}" = ["roles/billing.admin"]
}
}
##############
##############
##############
##-------------------------------------------------------------------------------
## 03 - Project Factory - TF Bucket, store Terraform state
##-------------------------------------------------------------------------------
module "d4science-project-factory-test-tfbucket" {
source = "../assets/modules-fabric/v26/gcs"
name = "d4science-test-ew8-foundation-tfprj-bkt"
project_id = module.d4science-seed-project.project_id
prefix = var.prefix
versioning = true
iam = {
"roles/storage.objectAdmin" = [module.d4science-project-factory-test-tfsa.iam_email]
}
location = "EUROPE-WEST8"
storage_class = "STANDARD"
labels = var.labels
}
module "d4science-project-factory-prod-tfbucket" {
source = "../assets/modules-fabric/v26/gcs"
name = "d4science-prod-ew8-foundation-tfprj-bkt"
project_id = module.d4science-seed-project.project_id
prefix = var.prefix
versioning = true
iam = {
"roles/storage.objectAdmin" = [module.d4science-project-factory-prod-tfsa.iam_email]
}
location = "EUROPE-WEST8"
storage_class = "STANDARD"
labels = var.labels
}

View File

@ -0,0 +1,73 @@
module "common-organization-monitoring-project" {
source = "../assets/modules-fabric/v26/project"
name = "d4science-com-monitoring-prj"
project_create = true
parent = "organizations/${var.organization.id}"
billing_account = var.billing_account_id
services = [
"cloudresourcemanager.googleapis.com", #required for SA impersonification
"iam.googleapis.com", #required for IAM and SA impersonification
"secretmanager.googleapis.com", #required for secrets
"monitoring.googleapis.com" #required for monitoring
]
iam = {
"roles/owner" = [
module.d4science-organization-tfsa.iam_email,
]
"roles/resourcemanager.projectIamAdmin" = [
module.d4science-project-factory-prod-tfsa.iam_email,
module.d4science-project-factory-test-tfsa.iam_email
],
"roles/monitoring.notificationChannelViewer" = [
module.d4science-project-factory-test-tfsa.iam_email,
module.d4science-project-factory-prod-tfsa.iam_email
]
}
group_iam = {
"foundationreply@d4science.org" = [
"roles/owner"
]
}
}
module "common-test-organization-monitoring-bucket" {
source = "../assets/modules-fabric/v26/logging-bucket"
parent_type = "project"
parent = module.common-organization-monitoring-project.project_id
id = "d4science-test-monitoring-sink-bucket"
location = "europe-west8"
description = "Terraform-managed."
}
module "common-prod-organization-monitoring-bucket" {
source = "../assets/modules-fabric/v26/logging-bucket"
parent_type = "project"
parent = module.common-organization-monitoring-project.project_id
id = "d4science-prod-monitoring-sink-bucket"
location = "europe-west4"
description = "Terraform-managed."
}
module "common-organization-monitoring-sa" {
source = "../assets/modules-fabric/v26/iam-service-account"
project_id = module.common-organization-monitoring-project.project_id
name = "d4science-com-monitoring-sa"
prefix = var.prefix
iam_organization_roles = {
"${var.organization.id}" = ["roles/monitoring.viewer"]
}
}
resource "google_monitoring_notification_channel" "budget_alerting" {
display_name = "Budget alerting"
type = "email"
project = module.common-organization-monitoring-project.project_id
labels = {
email_address = "budget@d4science.org"
}
user_labels = {}
}

View File

@ -0,0 +1,19 @@
##-------------------------------------------------------------------------------
## BCC_PAY - Organization
##-------------------------------------------------------------------------------
module "organization" {
source = "../assets/modules-fabric/v26/organization"
organization_id = "organizations/${var.organization.id}"
custom_roles = {
# Allow gke policy creation. Assign this to gke sa
"d4science_org_glb_gkemanagefwrules_role" = [
"compute.networks.updatePolicy",
"compute.firewalls.list",
"compute.firewalls.get",
"compute.firewalls.create",
"compute.firewalls.update",
"compute.firewalls.delete"
]
}
}

View File

@ -0,0 +1,38 @@
# tfdoc:file:description Providers output files.
locals {
providers = {
"00-organization" = {
bucket = module.d4science-organization-tfbucket.name
service_account = module.d4science-organization-tfsa.email
}
"01-networking" = {
bucket = module.d4science-networking-tfbucket.name
service_account = module.d4science-networking-tfsa.email
}
"02-security" = {
bucket = module.d4science-security-tfbucket.name
service_account = module.d4science-security-tfsa.email
}
"03-project-factory-test" = {
bucket = module.d4science-project-factory-test-tfbucket.name
service_account = module.d4science-project-factory-test-tfsa.email
}
"03-project-factory-prod" = {
bucket = module.d4science-project-factory-prod-tfbucket.name
service_account = module.d4science-project-factory-prod-tfsa.email
}
}
}
resource "local_file" "other_providers" {
for_each = var.outputs_location == null ? {} : local.providers
file_permission = "0644"
filename = "${path.module}/${var.outputs_location}/providers/${each.key}.providers.tf"
content = templatefile("${path.module}/../assets/providers.tpl", {
bucket = each.value.bucket
sa = each.value.service_account
prefix = try(each.value.prefix, null)
})
}

View File

@ -0,0 +1,63 @@
# tfdoc:file:description Terraform tfvars output files.
locals {
tfvars = {
billing_account_id = var.billing_account_id
organization = var.organization
prefix = var.prefix
labels = var.labels
groups = var.groups
# Global variables
seed-project = {
project_id = module.d4science-seed-project.project_id
project_number = module.d4science-seed-project.number
}
service_accounts = {
networking = module.d4science-networking-tfsa.iam_email
security = module.d4science-security-tfsa.iam_email
project_factory_test = module.d4science-project-factory-test-tfsa.iam_email
project_factory_prod = module.d4science-project-factory-prod-tfsa.iam_email
}
monitoring = {
bucket-sink-test = module.common-test-organization-monitoring-bucket
bucket-sink-prod = module.common-prod-organization-monitoring-bucket
project_id = module.common-organization-monitoring-project.project_id
channels = {
"budget-alerting" = google_monitoring_notification_channel.budget_alerting.id
}
}
# Folders
folders = {
networking = {
id = module.d4science-networking-folder.id
name = module.d4science-networking-folder.name
}
prod = {
id = module.d4science-prod-folder.id
name = module.d4science-prod-folder.name
}
test = {
id = module.d4science-test-folder.id
name = module.d4science-test-folder.name
}
}
}
}
resource "local_file" "tfvars" {
for_each = var.outputs_location == null ? {} : { 1 = 1 }
file_permission = "0644"
filename = "${path.module}/${var.outputs_location}/tfvars/00-organization.auto.tfvars.json"
content = jsonencode(local.tfvars)
}
output "tfvars" {
sensitive = true
value = local.tfvars
}

View File

@ -0,0 +1,18 @@
terraform {
backend "gcs" {
bucket = "d4science-com-ew8-foundation-tforg-bkt"
impersonate_service_account = "d4science-com-tforg-sa@d4science-com-automation-prj.iam.gserviceaccount.com"
}
}
provider "google" {
impersonate_service_account = "d4science-com-tforg-sa@d4science-com-automation-prj.iam.gserviceaccount.com"
}
provider "google-beta" {
impersonate_service_account = "d4science-com-tforg-sa@d4science-com-automation-prj.iam.gserviceaccount.com"
}
provider "google" {
alias = "no-impersonate"
}

View File

@ -0,0 +1,7 @@
#D4Science
billing_account_id = "018258-BCA804-E9D4C6"
organization = {
domain = "d4sscience.org"
id = 392184451762
}

View File

@ -0,0 +1,87 @@
module "d4science-seed-project" {
source = "../assets/modules-fabric/v26/project"
name = "d4science-com-automation-prj"
project_create = true
billing_account = var.billing_account_id
parent = "organizations/${var.organization.id}"
services = [
"cloudresourcemanager.googleapis.com", #required for SA impersonification
"serviceusage.googleapis.com", #required for SA impersonification
"iam.googleapis.com", #required for IAM and SA impersonification
"cloudbilling.googleapis.com", #required for project creation
"accesscontextmanager.googleapis.com", #required for VPC SC
"logging.googleapis.com", #required for sink creation
"servicenetworking.googleapis.com", #required for Private Service Access
"pubsub.googleapis.com", #required for PubSub
"monitoring.googleapis.com", #required for Monitoring
"billingbudgets.googleapis.com", #required for Billing budgets
"orgpolicy.googleapis.com" #required for Organizational policies
]
iam = {
"roles/owner" = [
module.d4science-organization-tfsa.iam_email,
]
}
group_iam = {
"foundationreply@d4science.org" = [
"roles/owner"
]
}
labels = var.labels
}
##-------------------------------------------------------------------------------
## Terraform bucket for storing TFSTATE
##-------------------------------------------------------------------------------
module "d4science-organization-tfbucket" {
source = "../assets/modules-fabric/v26/gcs"
name = "d4science-com-ew8-foundation-tforg-bkt"
project_id = module.d4science-seed-project.project_id
prefix = var.prefix
versioning = true
iam = {
"roles/storage.objectAdmin" = [module.d4science-organization-tfsa.iam_email]
}
# Maintain last 3 versions (1 live plus 2).
lifecycle_rules = {
limit-versions = {
action = {
type = "Delete"
}
condition = {
num_newer_versions = 3
with_state = "ARCHIVED"
}
}
}
location = "EUROPE-WEST8"
storage_class = "STANDARD"
labels = var.labels
}
##-------------------------------------------------------------------------------
## TF SA, impersonated to apply Terraform config
##-------------------------------------------------------------------------------
module "d4science-organization-tfsa" {
source = "../assets/modules-fabric/v26/iam-service-account"
project_id = module.d4science-seed-project.project_id
name = "d4science-com-tforg-sa"
prefix = var.prefix
iam = {
"roles/iam.serviceAccountTokenCreator" = ["group:foundationreply@d4science.org"]
#Impersonate service accounts (create OAuth2 access tokens, sign blobs or JWTs, etc).
}
}

View File

@ -0,0 +1,35 @@
variable "billing_account_id" {
description = "D4Science Billing account id."
type = string
}
variable "groups" {
description = "Group names to grant organization-level permissions."
type = map(string)
default = {}
}
variable "organization" {
description = "Organization details."
type = object({
domain = string
id = number
})
}
variable "prefix" {
description = "Prefix for terraform resources"
type = string
default = null
}
variable "outputs_location" {
description = "Assets path location relative to the module"
type = string
default = "../assets"
}
variable "labels" {
description = "Labels to add to the resources"
type = map(string)
default = {}
}

View File

@ -0,0 +1,9 @@
name : "d4science-prod-ew4-vpc-con-sub"
description : "Subnet assigned for D4Science prod environment's vpc connector"
region : "europe-west4"
ip_cidr_range : "10.254.2.0/28"
iam: # This section must be added after the creation of the service project to allow it to operate on the VPC
roles/compute.networkUser:
- serviceAccount:service-398661014261@gcp-sa-vpcaccess.iam.gserviceaccount.com
- serviceAccount:398661014261@cloudservices.gserviceaccount.com

View File

@ -0,0 +1,14 @@
name : "d4science-prod-ew4-vre-sub"
description : "Subnet assigned for D4Science prod environment's vre GKE cluster"
region : "europe-west4"
ip_cidr_range : "10.254.0.0/25"
secondary_ip_ranges: # map of secondary ip ranges
pods: 10.250.0.0/17
services: 10.250.128.0/17
iam: # This section must be added after the creation of the service project to allow it to operate on the VPC
roles/compute.networkUser:
- serviceAccount:service-783244529218@container-engine-robot.iam.gserviceaccount.com
- serviceAccount:783244529218@cloudservices.gserviceaccount.com

View File

@ -0,0 +1,9 @@
name : "d4science-test-ew8-vpc-con-sub"
description : "Subnet assigned for D4Science test environment's vpc connector"
region : "europe-west8"
ip_cidr_range : "10.254.66.0/28"
iam: # This section must be added after the creation of the service project to allow it to operate on the VPC
roles/compute.networkUser:
- serviceAccount:service-31481777243@gcp-sa-vpcaccess.iam.gserviceaccount.com
- serviceAccount:31481777243@cloudservices.gserviceaccount.com

View File

@ -0,0 +1,13 @@
name : "d4science-test-ew8-vre-sub"
description : "Subnet assigned for D4Science test environment's vre GKE cluster"
region : "europe-west8"
ip_cidr_range : "10.254.64.0/25"
secondary_ip_ranges: # map of secondary ip ranges
pods: 10.251.0.0/17
services: 10.251.128.0/17
iam: # This section must be added after the creation of the service project to allow it to operate on the VPC
roles/compute.networkUser:
- serviceAccount:service-804925782180@container-engine-robot.iam.gserviceaccount.com
- serviceAccount:804925782180@cloudservices.gserviceaccount.com

35
01-networking/main.tf Normal file
View File

@ -0,0 +1,35 @@
locals {
organization_vars = jsondecode(file("../assets/tfvars/00-organization.auto.tfvars.json"))
}
locals {
billing_account_id = local.organization_vars.billing_account_id
organization = local.organization_vars.organization
prefix = local.organization_vars.prefix
labels = local.organization_vars.labels
groups = local.organization_vars.groups
# Global variables
seed-project = {
project_id = local.organization_vars.seed-project.project_id
project_number = local.organization_vars.seed-project.project_number
}
service_accounts = {
networking = local.organization_vars.service_accounts.networking
security = local.organization_vars.service_accounts.security
project_factory_test = local.organization_vars.service_accounts.project_factory_test
project_factory_prod = local.organization_vars.service_accounts.project_factory_prod
}
# Folders
folders = {
networking = {
id = local.organization_vars.folders.networking.id
name = local.organization_vars.folders.networking.name
}
}
}

View File

@ -0,0 +1,48 @@
# tfdoc:file:description Terraform tfvars output files.
locals {
tfvars = {
spoke-test = {
project_id = module.d4science-networking-spoke-test-project.project_id
number = module.d4science-networking-spoke-test-project.number
network = module.d4science-networking-test-vpc.self_link
subnets = {
for k, v in module.d4science-networking-test-vpc.subnet_ips :
k => {
self_link = module.d4science-networking-test-vpc.subnets[k].self_link
ip = v,
secondary_ranges = module.d4science-networking-test-vpc.subnet_secondary_ranges[k]
}
}
nat-address = module.d4science-networking-spoke-test-addresses.external_addresses["nat-ew8-00-addr-00"].address
}
spoke-prod = {
project_id = module.d4science-networking-spoke-prod-project.project_id
number = module.d4science-networking-spoke-prod-project.number
network = module.d4science-networking-prod-vpc.self_link
subnets = {
for k, v in module.d4science-networking-prod-vpc.subnet_ips :
k => {
self_link = module.d4science-networking-prod-vpc.subnets[k].self_link
ip = v,
secondary_ranges = module.d4science-networking-prod-vpc.subnet_secondary_ranges[k]
}
}
nat-address = module.d4science-networking-spoke-prod-addresses.external_addresses["nat-ew4-00-addr-00"].address
}
}
}
resource "local_file" "tfvars" {
for_each = var.outputs_location == null ? {} : { 1 = 1 }
file_permission = "0644"
filename = "${path.module}/${var.outputs_location}/tfvars/01-networking.auto.tfvars.json"
content = jsonencode(local.tfvars)
}
output "tfvars" {
sensitive = true
value = local.tfvars
}

View File

@ -0,0 +1,37 @@
terraform {
backend "gcs" {
bucket = "d4science-com-ew8-foundation-tfnet-bkt"
impersonate_service_account = "d4science-com-tfnet-sa@d4science-com-automation-prj.iam.gserviceaccount.com"
}
required_version = "~> 1.6.5"
required_providers {
google = {
source = "hashicorp/google"
version = "4.84.0"
}
google-beta = {
source = "hashicorp/google-beta"
version = "4.84.0"
}
local = {
source = "hashicorp/local"
version = "~> 2.4.0"
}
}
}
provider "google" {
impersonate_service_account = "d4science-com-tfnet-sa@d4science-com-automation-prj.iam.gserviceaccount.com"
}
provider "google-beta" {
impersonate_service_account = "d4science-com-tfnet-sa@d4science-com-automation-prj.iam.gserviceaccount.com"
}
provider "google" {
alias = "no-impersonate"
}
provider "local" {}

117
01-networking/spoke-prod.tf Normal file
View File

@ -0,0 +1,117 @@
#----------------------------------
# Project
#----------------------------------
module "d4science-networking-spoke-prod-project" {
source = "../assets/modules-fabric/v26/project"
name = "d4science-prod-spoke-prj"
prefix = local.prefix
billing_account = local.billing_account_id
parent = local.folders.networking.id
labels = local.labels
auto_create_network = false
project_create = true
# Enable Shared VPC
shared_vpc_host_config = {
enabled = true
# First run the next line must be commented, after you execute step 3 you can uncomment it
# because you first need to create the service projects
service_projects = var.service_projects["prod"]
}
services = [
"vpcaccess.googleapis.com", # to enable VPC Access Connector
"container.googleapis.com",
"certificatemanager.googleapis.com", # required for certificates
"secretmanager.googleapis.com",
"servicenetworking.googleapis.com"
]
iam = {
"roles/owner" = []
"roles/editor" = ["serviceAccount:${module.d4science-networking-spoke-prod-project.service_accounts.cloud_services}"]
"roles/compute.securityAdmin" = [local.service_accounts.security] #to create firewall rules
#https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/tree/master/blueprints/factories/project-factory
#Required for Project Factory
"roles/browser" = [local.service_accounts.project_factory_prod]
"roles/compute.viewer" = [local.service_accounts.project_factory_prod]
"roles/dns.admin" = [local.service_accounts.project_factory_prod]
# These must be done after creating the service project
"roles/container.hostServiceAgentUser" = ["serviceAccount:service-783244529218@container-engine-robot.iam.gserviceaccount.com"]
"organizations/${local.organization_vars.organization.id}/roles/d4science_org_glb_gkemanagefwrules_role" = ["serviceAccount:service-783244529218@container-engine-robot.iam.gserviceaccount.com"]
}
}
#----------------------------------
# VPC prod with subnets
#----------------------------------
module "d4science-networking-prod-vpc" {
source = "../assets/modules-fabric/v26/net-vpc"
project_id = module.d4science-networking-spoke-prod-project.project_id
name = "d4science-prod-vpc"
factories_config = {
subnets_folder = "${path.root}/data/subnets/prod"
}
psa_config = {
ranges = { filestore = var.psa_ranges["prod"]["filestore"] }
}
}
#----------------------------------
# Network Adresses (GLB and NAT)
#----------------------------------
module "d4science-networking-spoke-prod-addresses" {
source = "../assets/modules-fabric/v26/net-address"
project_id = module.d4science-networking-spoke-prod-project.project_id
external_addresses = {
nat-ew4-00-addr-00 = { region = "europe-west4" }
}
}
#----------------------------------
# Cloud NAT (Spoke PROD)
#----------------------------------
module "d4science-networking-spoke-prod-cloudnat-ew4" {
source = "../assets/modules-fabric/v26/net-cloudnat"
project_id = module.d4science-networking-spoke-prod-project.project_id
region = "europe-west4"
name = "d4science-prod-ew4-01-nat"
addresses = [module.d4science-networking-spoke-prod-addresses.external_addresses["nat-ew4-00-addr-00"].self_link]
router_network = module.d4science-networking-prod-vpc.self_link
config_source_subnets = "LIST_OF_SUBNETWORKS"
subnetworks = [
{
self_link = module.d4science-networking-prod-vpc.subnet_self_links["europe-west4/d4science-prod-ew4-vre-sub"]
config_source_ranges = ["ALL_IP_RANGES"]
secondary_ranges = null
}
]
}
##----------------------------------
## Private Google Access (PROD)
##----------------------------------
module "d4science-networking-spoke-prod-pga" {
source = "../assets/modules-custom/pga"
project_id = module.d4science-networking-spoke-prod-project.project_id
name = "d4science-prod-pga"
domains = {
artifact = true
}
config = {
private = true
}
networks = [
module.d4science-networking-prod-vpc.self_link
]
}

117
01-networking/spoke-test.tf Normal file
View File

@ -0,0 +1,117 @@
#----------------------------------
# Project
#----------------------------------
module "d4science-networking-spoke-test-project" {
source = "../assets/modules-fabric/v26/project"
name = "d4science-test-spoke-prj"
prefix = local.prefix
billing_account = local.billing_account_id
parent = local.folders.networking.id
labels = local.labels
auto_create_network = false
project_create = true
# Enable Shared VPC
shared_vpc_host_config = {
enabled = true
# First run the next line must be commented, after you execute step 3 you can uncomment it
# because you first need to create the service projects
service_projects = var.service_projects["test"]
}
services = [
"vpcaccess.googleapis.com", # to enable VPC Access Connector
"container.googleapis.com",
"certificatemanager.googleapis.com", # required for certificates
"secretmanager.googleapis.com",
"servicenetworking.googleapis.com"
]
iam = {
"roles/owner" = []
"roles/editor" = ["serviceAccount:${module.d4science-networking-spoke-test-project.service_accounts.cloud_services}"]
"roles/compute.securityAdmin" = [local.service_accounts.security] #to create firewall rules
#https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/tree/master/blueprints/factories/project-factory
#Required for Project Factory
"roles/browser" = [local.service_accounts.project_factory_test]
"roles/compute.viewer" = [local.service_accounts.project_factory_test]
"roles/dns.admin" = [local.service_accounts.project_factory_test]
# These must be done after creating the service project
"roles/container.hostServiceAgentUser" = ["serviceAccount:service-804925782180@container-engine-robot.iam.gserviceaccount.com"]
"organizations/${local.organization_vars.organization.id}/roles/d4science_org_glb_gkemanagefwrules_role" = ["serviceAccount:service-804925782180@container-engine-robot.iam.gserviceaccount.com"]
}
}
#----------------------------------
# VPC test with subnets
#----------------------------------
module "d4science-networking-test-vpc" {
source = "../assets/modules-fabric/v26/net-vpc"
project_id = module.d4science-networking-spoke-test-project.project_id
name = "d4science-test-vpc"
factories_config = {
subnets_folder = "${path.root}/data/subnets/test"
}
psa_config = {
ranges = { filestore = var.psa_ranges["test"]["filestore"] }
}
}
#----------------------------------
# Network Adresses (GLB and NAT)
#----------------------------------
module "d4science-networking-spoke-test-addresses" {
source = "../assets/modules-fabric/v26/net-address"
project_id = module.d4science-networking-spoke-test-project.project_id
external_addresses = {
nat-ew8-00-addr-00 = { region = "europe-west8" }
}
}
#----------------------------------
# Cloud NAT (Spoke TEST)
#----------------------------------
module "d4science-networking-spoke-test-cloudnat-ew8" {
source = "../assets/modules-fabric/v26/net-cloudnat"
project_id = module.d4science-networking-spoke-test-project.project_id
region = "europe-west8"
name = "d4science-test-ew8-01-nat"
addresses = [module.d4science-networking-spoke-test-addresses.external_addresses["nat-ew8-00-addr-00"].self_link]
router_network = module.d4science-networking-test-vpc.self_link
config_source_subnets = "LIST_OF_SUBNETWORKS"
subnetworks = [
{
self_link = module.d4science-networking-test-vpc.subnet_self_links["europe-west8/d4science-test-ew8-vre-sub"]
config_source_ranges = ["ALL_IP_RANGES"]
secondary_ranges = null
}
]
}
##----------------------------------
## Private Google Access (TEST)
##----------------------------------
module "d4science-networking-spoke-test-pga" {
source = "../assets/modules-custom/pga"
project_id = module.d4science-networking-spoke-test-project.project_id
name = "d4science-test-pga"
domains = {
artifact = true
}
config = {
private = true
}
networks = [
module.d4science-networking-test-vpc.self_link
]
}

View File

@ -0,0 +1,10 @@
service_projects = {
networking : ["d4science-spoke-prod", "d4science-spoke-test"],
test : ["d4science-test-vre-prj", "d4science-test-script-prj"],
prod : ["d4science-prod-vre-prj", "d4science-prod-script-prj"]
}
psa_ranges = {
"test" : { "filestore" : "10.254.65.0/24" },
"prod" : { "filestore" : "10.254.1.0/24" },
}

View File

@ -0,0 +1,24 @@
variable "outputs_location" {
description = "Assets path location relative to the module"
type = string
default = "../assets"
}
variable "service_projects" {
description = "A map of lists reporting projects to attach to a shared VPC of each environment"
type = map(list(string))
default = {
networking : [],
test : [],
prod : []
}
}
variable "psa_ranges" {
description = "A map of maps of address ranges for private service access"
type = map(map(string))
default = {
test : {}
prod : {}
}
}

View File

@ -0,0 +1,37 @@
health-check:
- 35.191.0.0/16
- 130.211.0.0/22
- 108.170.220.0/23
vpc-nat-ranges:
- 107.178.230.64/26
- 35.199.224.0/19
google-restricted-api:
- 199.36.153.4/30
google-private-api:
- 199.36.153.8/30
#----------------------------------
# VRE
#----------------------------------
# TEST
d4science-test-vre-gke-controlplane:
- 10.249.0.64/28
d4science-test-vre-gke-pods:
- 10.251.0.0/17
d4science-test-vre-gke-services:
- 10.251.128.0/17
d4science-test-vre-gke-subnet:
- 10.254.64.0/25
# PROD
d4science-prod-vre-gke-controlplane:
- 10.249.0.0/28
d4science-prod-vre-gke-pods:
- 10.250.0.0/17
d4science-prod-vre-gke-services:
- 10.250.128.0/17
d4science-prod-vre-gke-subnet:
- 10.254.0.0/25

View File

@ -0,0 +1,7 @@
# Cloud Functions ORG policies
cloudfunctions.allowedIngressSettings:
rules:
- allow:
values:
- ALLOW_INTERNAL_ONLY

View File

@ -0,0 +1,18 @@
# GCP ORG policies
gcp.detailedAuditLoggingMode:
rules:
- enforce: true
gcp.resourceLocations:
rules:
- allow:
values:
- in:eu-locations
gcp.restrictTLSVersion:
rules:
- deny:
values:
- TLS_VERSION_1
- TLS_VERSION_1_1

View File

@ -0,0 +1,15 @@
# IAM ORG policies
iam.disableServiceAccountKeyCreation:
rules:
- enforce: true
iam.disableServiceAccountKeyUpload:
rules:
- enforce: true
iam.allowedPolicyMemberDomains:
rules:
- allow:
values:
- C01slynf8

View File

@ -0,0 +1,25 @@
# Cloud Run ORG policies
# Dopo svariati test, l'unica sintassi funzionante necessita di
# due regole condizionali, con condizione opposta
# Warning: al tempo del test (08/2023) la UI di Cloud Run risulta buggata
# Testare Ingress tramite deploy gcloud
run.allowedIngress:
rules:
- allow:
values:
- internal
- internal-and-cloud-load-balancing
condition:
description: "Condizione per *non* permettere Ingress di tipo \"ALL\" ai servizi Cloud Run"
expression: "!resource.matchTag(\"812186410362/cloudrun-allowIngress-tag\", \"allow-all\")"
title: "Deny Ingress \"ALL\""
- allow:
values:
- all
- internal
- internal-and-cloud-load-balancing
condition:
description: "Condizione per permettere Ingress di tipo \"ALL\" ai servizi Cloud Run"
expression: "resource.matchTag(\"812186410362/cloudrun-allowIngress-tag\", \"allow-all\")"
title: "Allow Ingress \"ALL\""

View File

@ -0,0 +1,10 @@
# Storage ORG policies
storage.publicAccessPrevention:
rules:
- enforce: true
storage.retentionPolicySeconds:
rules:
- allow:
all: true

View File

@ -0,0 +1,21 @@
ingress:
deny-ingress-all:
deny: true
description: "Block ingress."
priority: 65000
enable_logging:
include_metadata: true
rules:
- protocol: all
ports: []
egress:
deny-egress-all:
deny: true
description: "Block egress."
priority: 65001
enable_logging:
include_metadata: true
rules:
- protocol: all
ports: []

View File

@ -0,0 +1,103 @@
ingress:
d4science-prod-glb-vre-ig-alw-healthcheck-fwr:
deny: false
description: Allow Healthcheck.
priority: 1000
source_ranges:
- health-check
rules:
- protocol: tcp
ports:
- 80
- 443
- 667
- 8080
d4science-prod-glb-ing-alw-gkecontrolplane-fwr:
deny: false
description: "Allow GKE master to reach VPC."
priority: 1000
source_ranges:
- d4science-prod-vre-gke-controlplane
rules:
- protocol: all
ports: []
d4science-prod-glb-vre-ing-serverless-to-vpc-connector:
deny: false
description: Allow serverless services to reach vpc connectors.
priority: 1000
source_ranges:
- vpc-nat-ranges
rules:
- protocol: tcp
ports:
- 667
- protocol: udp
ports:
- 665
- 666
- protocol: icmp
ports: []
egress:
d4science-prod-glb-vre-egr-alw-dns-fwr:
deny: false
description: Allow DNS Egress.
priority: 1000
rules:
- protocol: tcp
ports: [53]
- protocol: udp
ports: [53]
d4science-prod-glb-vre-egr-alw-private-fwr:
deny: false
description: Allow PGA.
priority: 1000
destination_ranges:
- google-private-api
rules:
- protocol: tcp
ports: [443]
# This rule can be enforced only after the SA is created inside the service project. When you first create the cluster, activate it without the options for enforcing the SA.
d4science-prod-glb-vre-egr-alw-cluster-fwr:
deny: false
description: "Allow cluster communication."
use_service_accounts: true
targets:
- d4science-prod-vre-nodepoolsa@d4science-prod-vre-prj.iam.gserviceaccount.com
destination_ranges:
- 0.0.0.0/0
priority: 1000
rules:
- protocol: all
ports: []
d4science-prod-glb-vre-egr-vpc-connector-to-gke:
deny: false
description: "Allow vpc serverless connector to gke control plane communication."
targets:
- vpc-connector-europe-west4-d4science-prod-cf-vpc-con
destination_ranges:
- 0.0.0.0/0
priority: 1000
rules:
- protocol: all
ports: []
d4science-prod-glb-vre-egr-vpc-connector-to-serverless:
deny: false
description: Allow vpc connectors to reach serverless services.
priority: 1000
destination_ranges:
- vpc-nat-ranges
rules:
- protocol: tcp
ports:
- 667
- protocol: udp
ports:
- 665
- 666
- protocol: icmp
ports: []

View File

@ -0,0 +1,21 @@
ingress:
deny-ingress-all:
deny: true
description: "Block ingress."
priority: 65000
enable_logging:
include_metadata: true
rules:
- protocol: all
ports: []
egress:
deny-egress-all:
deny: true
description: "Block egress."
priority: 65001
enable_logging:
include_metadata: true
rules:
- protocol: all
ports: []

View File

@ -0,0 +1,103 @@
ingress:
d4science-test-glb-vre-ig-alw-healthcheck-fwr:
deny: false
description: Allow Healthcheck.
priority: 1000
source_ranges:
- health-check
rules:
- protocol: tcp
ports:
- 80
- 443
- 667
- 8080
d4science-test-glb-ing-alw-gkecontrolplane-fwr:
deny: false
description: "Allow GKE master to reach VPC."
priority: 1000
source_ranges:
- d4science-test-vre-gke-controlplane
rules:
- protocol: all
ports: []
d4science-test-glb-vre-ing-serverless-to-vpc-connector:
deny: false
description: Allow serverless services to reach vpc connectors.
priority: 1000
source_ranges:
- vpc-nat-ranges
rules:
- protocol: tcp
ports:
- 667
- protocol: udp
ports:
- 665
- 666
- protocol: icmp
ports: []
egress:
d4science-test-glb-vre-egr-alw-dns-fwr:
deny: false
description: Allow DNS Egress.
priority: 1000
rules:
- protocol: tcp
ports: [53]
- protocol: udp
ports: [53]
d4science-test-glb-vre-egr-alw-private-fwr:
deny: false
description: Allow PGA.
priority: 1000
destination_ranges:
- google-private-api
rules:
- protocol: tcp
ports: [443]
# This rule can be enforced only after the SA is created inside the service project. When you first create the cluster, activate it without the options for enforcing the SA.
d4science-test-glb-vre-egr-alw-cluster-fwr:
deny: false
description: "Allow cluster communication."
use_service_accounts: true
targets:
- d4science-test-vre-nodepoolsa@d4science-test-vre-prj.iam.gserviceaccount.com
destination_ranges:
- 0.0.0.0/0
priority: 1000
rules:
- protocol: all
ports: []
d4science-test-glb-vre-egr-vpc-connector-to-gke:
deny: false
description: "Allow vpc serverless connector to gke control plane communication."
targets:
- vpc-connector-europe-west8-d4science-test-cf-vpc-con
destination_ranges:
- 0.0.0.0/0
priority: 1000
rules:
- protocol: all
ports: []
d4science-test-glb-vre-egr-vpc-connector-to-serverless:
deny: false
description: Allow vpc connectors to reach serverless services.
priority: 1000
destination_ranges:
- vpc-nat-ranges
rules:
- protocol: tcp
ports:
- 667
- protocol: udp
ports:
- 665
- 666
- protocol: icmp
ports: []

13
02-security/main.tf Normal file
View File

@ -0,0 +1,13 @@
locals {
networking_vars = jsondecode(file("../assets/tfvars/01-networking.auto.tfvars.json"))
}
locals {
spoke-test = {
project_id = local.networking_vars.spoke-test.project_id
network = local.networking_vars.spoke-test.network
}
spoke-prod = {
project_id = local.networking_vars.spoke-prod.project_id
network = local.networking_vars.spoke-prod.network
}
}

View File

@ -0,0 +1,8 @@
##-------------------------------------------------------------------------------
## 02-Security - Organization - ORG POLICIES
##-------------------------------------------------------------------------------
module "org" {
source = "../assets/modules-fabric/v26/organization"
organization_id = "organizations/${var.organization.id}"
org_policies_data_path = "firewall/rules/org-policies"
}

37
02-security/providers.tf Normal file
View File

@ -0,0 +1,37 @@
terraform {
backend "gcs" {
bucket = "d4science-com-ew8-foundation-tfsec-bkt"
impersonate_service_account = "d4science-com-tfsec-sa@d4science-com-automation-prj.iam.gserviceaccount.com"
}
required_version = "~> 1.6.5"
required_providers {
google = {
source = "hashicorp/google"
version = "4.84.0"
}
google-beta = {
source = "hashicorp/google-beta"
version = "4.84.0"
}
local = {
source = "hashicorp/local"
version = "~> 2.4.0"
}
}
}
provider "google" {
impersonate_service_account = "d4science-com-tfsec-sa@d4science-com-automation-prj.iam.gserviceaccount.com"
}
provider "google-beta" {
impersonate_service_account = "d4science-com-tfsec-sa@d4science-com-automation-prj.iam.gserviceaccount.com"
}
provider "google" {
alias = "no-impersonate"
}
provider "local" {}

View File

@ -0,0 +1,17 @@
#----------------------------------
# Spoke PROD Firewall
#----------------------------------
module "d4science-security-spoke-prod-firewall" {
source = "../assets/modules-fabric/v26/net-vpc-firewall"
project_id = local.spoke-prod.project_id
network = local.spoke-prod.network
default_rules_config = {
disabled = true
}
factories_config = {
rules_folder = "firewall/rules/spoke-prod"
cidr_tpl_file = "firewall/cidrs.yaml"
}
}

View File

@ -0,0 +1,17 @@
#----------------------------------
# Spoke TEST Firewall
#----------------------------------
module "d4science-security-spoke-test-firewall" {
source = "../assets/modules-fabric/v26/net-vpc-firewall"
project_id = local.spoke-test.project_id
network = local.spoke-test.network
default_rules_config = {
disabled = true
}
factories_config = {
rules_folder = "firewall/rules/spoke-test"
cidr_tpl_file = "firewall/cidrs.yaml"
}
}

View File

@ -0,0 +1,4 @@
organization = {
domain = "d4sscience.org"
id = 392184451762
}

7
02-security/variables.tf Normal file
View File

@ -0,0 +1,7 @@
variable "organization" {
description = "Organization details."
type = object({
domain = string
id = number
})
}

View File

@ -0,0 +1,45 @@
locals {
organization_vars = jsondecode(file("../assets/tfvars/00-organization.auto.tfvars.json"))
networking_vars = jsondecode(file("../assets/tfvars/01-networking.auto.tfvars.json"))
}
locals {
billing_account_id = local.organization_vars.billing_account_id
organization = local.organization_vars.organization
prefix = local.organization_vars.prefix
labels = local.organization_vars.labels
groups = local.organization_vars.groups
# Global variables
seed-project = {
project_id = local.organization_vars.seed-project.project_id
project_number = local.organization_vars.seed-project.project_number
}
service_accounts = {
networking = local.organization_vars.service_accounts.networking
security = local.organization_vars.service_accounts.security
project_factory_prod = local.organization_vars.service_accounts.project_factory_prod
}
networking = {
spoke-prod-project = local.networking_vars.spoke-prod
}
monitoring = {
bucket-sink = local.organization_vars.monitoring.bucket-sink-prod
project_id = local.organization_vars.monitoring.project_id
channels = local.organization_vars.monitoring.channels
}
# Folders
folders = {
prod = {
id = local.organization_vars.folders.prod.id
name = local.organization_vars.folders.prod.name
}
}
}

View File

@ -0,0 +1,37 @@
terraform {
backend "gcs" {
bucket = "d4science-prod-ew8-foundation-tfprj-bkt"
impersonate_service_account = "d4science-prod-tfprj-sa@d4science-com-automation-prj.iam.gserviceaccount.com"
}
required_version = "~> 1.6.5"
required_providers {
google = {
source = "hashicorp/google"
version = "4.84.0"
}
google-beta = {
source = "hashicorp/google-beta"
version = "4.84.0"
}
local = {
source = "hashicorp/local"
version = "~> 2.4.0"
}
}
}
provider "google" {
impersonate_service_account = "d4science-prod-tfprj-sa@d4science-com-automation-prj.iam.gserviceaccount.com"
}
provider "google-beta" {
impersonate_service_account = "d4science-prod-tfprj-sa@d4science-com-automation-prj.iam.gserviceaccount.com"
}
provider "google" {
alias = "no-impersonate"
}
provider "local" {}

View File

@ -0,0 +1,477 @@
module "d4science-prod-budget-script-project" {
source = "../assets/modules-fabric/v26/project"
name = "d4science-prod-script-prj"
prefix = local.prefix
billing_account = local.billing_account_id
parent = local.folders.prod.id
labels = local.labels
auto_create_network = false
project_create = true
services = [
"cloudresourcemanager.googleapis.com", #required for SA impersonification
"iam.googleapis.com", #required for IAM and SA impersonification
"cloudfunctions.googleapis.com", #required for Cloud Functions
"pubsub.googleapis.com", #required for PubSub
"billingbudgets.googleapis.com", #required for Billing alerting,
"run.googleapis.com", #required for Cloud Functions 2nd gen
"cloudbuild.googleapis.com", #required for Cloud Functions 2nd gen
"eventarc.googleapis.com", #required for Cloud Functions 2nd gen
"vpcaccess.googleapis.com", #required for Serverless VPC access
]
iam = {
"roles/owner" = [
local.service_accounts.project_factory_prod,
]
}
group_iam = {
"foundationreply@d4science.org" = [
"roles/owner"
]
}
}
module "d4science-prod-budget-alert-topic" {
source = "../assets/modules-fabric/v26/pubsub"
project_id = module.d4science-prod-budget-script-project.project_id
name = "d4science-prod-budget-alerting-topic"
}
module "d4science-billing-budget-overall-year-critical" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Critical - Overall yearly budget"
amount = 442500
currency_code = "EUR"
thresholds = {
current = [0.95, 0.97]
forecasted = []
}
calendar_period = "YEAR"
resource_ancestors = ["organizations/${local.organization.id}"]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
module "d4science-billing-budget-overall-year" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Overall yearly budget"
amount = 442500
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 0.9]
forecasted = []
}
calendar_period = "YEAR"
resource_ancestors = ["organizations/${local.organization.id}"]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
module "d4science-billing-budget-overall-month" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Overall monthly budget"
amount = 36875
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
calendar_period = "MONTH"
resource_ancestors = ["organizations/${local.organization.id}"]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
module "d4science-prod-billing-budget-blue-cloud-total-year" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - BlueCloud cumulative yearly budget"
amount = 168000
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-namespace" = "blue-cloud"
}
calendar_period = "YEAR"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
pubsub_topic = module.d4science-prod-budget-alert-topic.id # Attention: this cannot be done without temporarily deactivating the org policy iam.allowedPolicyMemberDomains
}
module "d4science-prod-billing-budget-i-marine-total-year" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - iMarine cumulative yearly budget"
amount = 40500
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-namespace" = "i-marine"
}
calendar_period = "YEAR"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
pubsub_topic = module.d4science-prod-budget-alert-topic.id # Attention: this cannot be done without temporarily deactivating the org policy iam.allowedPolicyMemberDomains
}
module "d4science-prod-billing-budget-so-big-data-total-year" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - SoBigData cumulative yearly budget"
amount = 147000
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-namespace" = "so-big-data"
}
calendar_period = "YEAR"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
pubsub_topic = module.d4science-prod-budget-alert-topic.id # Attention: this cannot be done without temporarily deactivating the org policy iam.allowedPolicyMemberDomains
}
module "d4science-prod-billing-budget-open-community-total-year" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - OpenCommunity cumulative yearly budget"
amount = 87000
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-namespace" = "open-community"
}
calendar_period = "YEAR"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
pubsub_topic = module.d4science-prod-budget-alert-topic.id # Attention: this cannot be done without temporarily deactivating the org policy iam.allowedPolicyMemberDomains
}
module "d4science-prod-billing-budget-blue-cloud-year" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - BlueCloud namespace yearly budget"
amount = 117000
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-label/d4science-namespace" = "blue-cloud"
}
calendar_period = "YEAR"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
module "d4science-prod-billing-budget-i-marine-year" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - iMarine namespace yearly budget"
amount = 22500
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-label/d4science-namespace" = "i-marine"
}
calendar_period = "YEAR"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
module "d4science-prod-billing-budget-so-big-data-year" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - SoBigData namespace yearly budget"
amount = 117000
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-label/d4science-namespace" = "so-big-data"
}
calendar_period = "YEAR"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
module "d4science-prod-billing-budget-open-community-year" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - OpenCommunity namespace yearly budget"
amount = 87000
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-label/d4science-namespace" = "open-community"
}
calendar_period = "YEAR"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
module "d4science-prod-billing-budget-blue-cloud-datathon-year" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - BlueCloudDatathon namespace yearly budget"
amount = 51000
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-label/d4science-namespace" = "blue-cloud-datathon"
}
calendar_period = "YEAR"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
module "d4science-prod-billing-budget-i-marine-datathon-year" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - iMarineDatathon namespace yearly budget"
amount = 18000
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-label/d4science-namespace" = "i-marine-datathon"
}
calendar_period = "YEAR"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
module "d4science-prod-billing-budget-so-big-data-datathon-year" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - SoBigDataDatathon namespace yearly budget"
amount = 30000
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-label/d4science-namespace" = "so-big-data-datathon"
}
calendar_period = "YEAR"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
# module "d4science-prod-billing-budget-open-community-year" {
# source = "../assets/modules-custom/billing-budget"
# billing_account = local.billing_account_id
# name = "Prod - GKE - OpenCommunityDatathon namespace yearly budget"
# amount = 0
# currency_code = "EUR"
# thresholds = {
# current = [0.5, 0.8, 1.0]
# forecasted = []
# }
# labels = {
# "k8s-label/d4science-namespace" = "open-community-datathon"
# }
# calendar_period = "YEAR"
# resource_ancestors = [local.folders.prod.id]
# notification_channels = [local.monitoring.channels["budget-alerting"]]
# }
module "d4science-prod-billing-budget-blue-cloud-month" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - BlueCloud namespace monthly budget"
amount = 9750
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-label/d4science-namespace" = "blue-cloud"
}
calendar_period = "MONTH"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
module "d4science-prod-billing-budget-i-marine-month" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - iMarine namespace monthly budget"
amount = 1875
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-label/d4science-namespace" = "i-marine"
}
calendar_period = "MONTH"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
module "d4science-prod-billing-budget-so-big-data-month" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - SoBigData namespace monthly budget"
amount = 9750
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-label/d4science-namespace" = "so-big-data"
}
calendar_period = "MONTH"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
module "d4science-prod-billing-budget-open-community-month" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - OpenCommunity namespace monthly budget"
amount = 7250
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-label/d4science-namespace" = "open-community"
}
calendar_period = "MONTH"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
module "d4science-prod-billing-budget-blue-cloud-datathon-month" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - BlueCloudDatathon namespace monthly budget"
amount = 4250
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-label/d4science-namespace" = "blue-cloud-datathon"
}
calendar_period = "MONTH"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
module "d4science-prod-billing-budget-i-marine-datathon-month" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - iMarineDatathon namespace monthly budget"
amount = 1500
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-label/d4science-namespace" = "i-marine-datathon"
}
calendar_period = "MONTH"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
module "d4science-prod-billing-budget-so-big-data-datathon-month" {
source = "../assets/modules-custom/billing-budget"
billing_account = local.billing_account_id
name = "Prod - GKE - SoBigDataDatathon namespace monthly budget"
amount = 2500
currency_code = "EUR"
thresholds = {
current = [0.5, 0.8, 1.0]
forecasted = []
}
labels = {
"k8s-label/d4science-namespace" = "so-big-data-datathon"
}
calendar_period = "MONTH"
resource_ancestors = [local.folders.prod.id]
notification_channels = [local.monitoring.channels["budget-alerting"]]
}
# module "d4science-prod-billing-budget-open-community-datathon-month" {
# source = "../assets/modules-custom/billing-budget"
# billing_account = local.billing_account_id
# name = "Prod - GKE - OpenCommunityDatathon namespace monthly budget"
# amount = 0
# currency_code = "EUR"
# thresholds = {
# current = [0.5, 0.8, 1.0]
# forecasted = []
# }
# labels = {
# "k8s-label/d4science-namespace" = "open-community-datathon"
# }
# calendar_period = "MONTH"
# resource_ancestors = [local.folders.prod.id]
# notification_channels = [local.monitoring.channels["budget-alerting"]]
# }
module "d4science-prod-budget-script-sa" {
source = "../assets/modules-fabric/v26/iam-service-account"
project_id = module.d4science-prod-budget-script-project.project_id
name = "d4science-prod-budget-cf-sa"
iam = {
"roles/iam.serviceAccountTokenCreator" = ["group:foundationreply@d4science.org"]
#Impersonate service accounts (create OAuth2 access tokens, sign blobs or JWTs, etc).
}
iam_billing_roles = {
"${local.billing_account_id}" = ["roles/billing.viewer"]
}
}
resource "google_vpc_access_connector" "d4science-prod-budget-script-vpc-connector" {
project = module.d4science-prod-budget-script-project.project_id
name = "d4science-prod-cf-vpc-con"
region = var.gke_region
subnet {
name = "d4science-prod-ew4-vpc-con-sub"
project_id = local.networking.spoke-prod-project.project_id
}
}

View File

@ -0,0 +1,13 @@
gke_region = "europe-west4"
gke_subnet_id = "europe-west4/d4science-prod-ew4-vre-sub"
gke_master_auth_networks = {
"cnr-d4science-1" = "146.48.28.0/22",
"cnr-d4science-2" = "146.48.122.0/23",
"cnr-d4science-3" = "145.90.225.224/27",
# "cnr-d4science-4" = "2001:610:450:80::/64",
"reply-vpn-1" = "91.218.224.5/32",
"reply-vpn-2" = "91.218.224.15/32",
"reply-vpn-3" = "91.218.226.5/32",
"reply-vpn-4" = "91.218.226.15/32",
"budget-cf-vpc-access" = "10.254.2.0/28"
}

View File

@ -0,0 +1,23 @@
variable "outputs_location" {
description = "Assets path location relative to the module"
type = string
default = "../assets"
}
variable "gke_subnet_id" {
description = "Id of the subnet where gke cluster must reside"
type = string
default = ""
}
variable "gke_region" {
description = "Region where gke cluster is located"
type = string
default = ""
}
variable "gke_master_auth_networks" {
description = "Networks which are allowed to access gke control plane"
type = map(string)
default = {}
}

View File

@ -0,0 +1,96 @@
module "d4science-prod-vre-gke-cluster" {
source = "../assets/modules-fabric/v26/gke-cluster-standard"
project_id = module.d4science-prod-vre-project.project_id
name = "d4science-prod-vre-gke-cluster"
location = var.gke_region
vpc_config = {
network = local.networking.spoke-prod-project.network
subnetwork = local.networking.spoke-prod-project.subnets[var.gke_subnet_id].self_link
secondary_range_names = {
pods = "pods"
services = "services"
}
master_authorized_ranges = var.gke_master_auth_networks
master_ipv4_cidr_block = "10.249.0.0/28"
}
private_cluster_config = {
enable_private_endpoint = false
master_global_access = false
}
enable_features = {
dataplane_v2 = true
workload_identity = true
cost_management = true
}
enable_addons = {
gce_persistent_disk_csi_driver = true
gcp_filestore_csi_driver = true
horizontal_pod_autoscaling = true
http_load_balancing = true
}
logging_config = {
enable_workloads_logs = true
enable_api_server_logs = true
enable_scheduler_logs = true
enable_controller_manager_logs = true
}
monitoring_config = {
enable_api_server_metrics = true
enable_controller_manager_metrics = true
enable_scheduler_metrics = true
enable_daemonset_metrics = true
enable_deployment_metrics = true
enable_hpa_metrics = true
enable_pod_metrics = true
enable_statefulset_metrics = true
enable_storage_metrics = true
}
maintenance_config = {
daily_window_start_time = null
recurring_window = {
start_time = "2024-02-17T01:00:00Z"
end_time = "2024-02-17T05:00:00Z"
recurrence = "FREQ=WEEKLY;BYDAY=SA,SU,MO"
}
}
labels = {
environment = "prod"
}
}
module "d4science-prod-vre-gke-nodepool" {
source = "../assets/modules-fabric/v26/gke-nodepool"
project_id = module.d4science-prod-vre-project.project_id
cluster_name = module.d4science-prod-vre-gke-cluster.name
location = var.gke_region
name = "d4science-prod-vre-gke-nodepool"
service_account = {
create = true
email = "d4science-prod-vre-nodepoolsa"
oauth_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
}
node_config = {
machine_type = "custom-20-73728"
gcfs = true
}
nodepool_config = {
autoscaling = {
max_node_count = 30
min_node_count = 1
}
management = {
auto_repair = true
auto_upgrade = true
}
}
}
module "d4science_prod_artifact_registry" {
source = "../assets/modules-fabric/v26/artifact-registry"
project_id = module.d4science-prod-vre-project.project_id
location = var.gke_region
name = "d4science-prod-images"
iam = {
"roles/artifactregistry.reader" = [module.d4science-prod-vre-gke-nodepool.service_account_iam_email]
}
}

View File

@ -0,0 +1,105 @@
module "d4science-prod-vre-project" {
source = "../assets/modules-fabric/v26/project"
name = "d4science-prod-vre-prj"
prefix = local.prefix
billing_account = local.billing_account_id
parent = local.folders.prod.id
labels = local.labels
auto_create_network = false
project_create = true
services = [
"cloudresourcemanager.googleapis.com", #required for SA impersonification
"iam.googleapis.com", #required for IAM and SA impersonification
"container.googleapis.com", #required for GKE
"artifactregistry.googleapis.com", #required for Artifact registry
"pubsub.googleapis.com", #required for PubSub
"billingbudgets.googleapis.com", #required for Billing alerting
"certificatemanager.googleapis.com", #required for certificates
"secretmanager.googleapis.com", #required for secrets
"logging.googleapis.com", #required for logging
"file.googleapis.com", #required for Filestore
"servicenetworking.googleapis.com",
"containerfilesystem.googleapis.com" #required for image streaming in GKE
]
iam = {
"roles/owner" = [
local.service_accounts.project_factory_prod,
]
"roles/container.developer" = ["serviceAccount:${module.d4science-prod-budget-script-sa.email}"]
}
group_iam = {
"foundationreply@d4science.org" = [
"roles/owner"
]
}
logging_sinks = {
gke = {
destination = local.monitoring.bucket-sink.id
filter = "resource.type=\"k8s_cluster\""
type = "logging"
unique_writer = true
}
}
}
module "d4science-prod-vre-addresses" {
source = "../assets/modules-fabric/v26/net-address"
project_id = module.d4science-prod-vre-project.project_id
global_addresses = ["d4science-prod-vre-address-ext"]
}
module "d4science-prod-vre-ssl-secret" {
source = "../assets/modules-fabric/v26/secret-manager"
project_id = module.d4science-prod-vre-project.project_id
secrets = {
# Must be uploaded manually
d4science-prod-vre-ssl-private-key = ["europe-west4"],
d4science-prod-vre-ssl-public-cert = ["europe-west4"]
}
}
resource "google_compute_ssl_policy" "d4science-prod-vre-ssl-policy-1_2-modern" {
project = module.d4science-prod-vre-project.project_id
name = "d4science-prod-ssl-policy-1-2-modern"
profile = "MODERN"
min_tls_version = "TLS_1_2"
}
resource "google_compute_security_policy" "d4science-prod-vre-armor-policy" {
project = module.d4science-prod-vre-project.project_id
name = "d4science-prod-vre-armor-policy"
adaptive_protection_config {
layer_7_ddos_defense_config {
enable = true
}
}
rule {
action = "deny(403)"
description = "Block RU"
priority = "1000"
match {
expr {
expression = "origin.region_code == 'RU'"
}
}
}
rule {
action = "allow"
priority = "2147483647"
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["*"]
}
}
description = "default rule"
}
}

View File

@ -0,0 +1,44 @@
locals {
organization_vars = jsondecode(file("../assets/tfvars/00-organization.auto.tfvars.json"))
networking_vars = jsondecode(file("../assets/tfvars/01-networking.auto.tfvars.json"))
}
locals {
billing_account_id = local.organization_vars.billing_account_id
organization = local.organization_vars.organization
prefix = local.organization_vars.prefix
labels = local.organization_vars.labels
groups = local.organization_vars.groups
# Global variables
seed-project = {
project_id = local.organization_vars.seed-project.project_id
project_number = local.organization_vars.seed-project.project_number
}
service_accounts = {
networking = local.organization_vars.service_accounts.networking
security = local.organization_vars.service_accounts.security
project_factory_test = local.organization_vars.service_accounts.project_factory_test
}
networking = {
spoke-test-project = local.networking_vars.spoke-test
}
monitoring = {
bucket-sink = local.organization_vars.monitoring.bucket-sink-test
project_id = local.organization_vars.monitoring.project_id
channels = local.organization_vars.monitoring.channels
}
# Folders
folders = {
test = {
id = local.organization_vars.folders.test.id
name = local.organization_vars.folders.test.name
}
}
}

View File

@ -0,0 +1,37 @@
terraform {
backend "gcs" {
bucket = "d4science-test-ew8-foundation-tfprj-bkt"
impersonate_service_account = "d4science-test-tfprj-sa@d4science-com-automation-prj.iam.gserviceaccount.com"
}
required_version = "~> 1.6.5"
required_providers {
google = {
source = "hashicorp/google"
version = "4.84.0"
}
google-beta = {
source = "hashicorp/google-beta"
version = "4.84.0"
}
local = {
source = "hashicorp/local"
version = "~> 2.4.0"
}
}
}
provider "google" {
impersonate_service_account = "d4science-test-tfprj-sa@d4science-com-automation-prj.iam.gserviceaccount.com"
}
provider "google-beta" {
impersonate_service_account = "d4science-test-tfprj-sa@d4science-com-automation-prj.iam.gserviceaccount.com"
}
provider "google" {
alias = "no-impersonate"
}
provider "local" {}

View File

@ -0,0 +1,177 @@
module "d4science-test-budget-script-project" {
source = "../assets/modules-fabric/v26/project"
name = "d4science-test-script-prj"
prefix = local.prefix
billing_account = local.billing_account_id
parent = local.folders.test.id
labels = local.labels
auto_create_network = false
project_create = true
services = [
"cloudresourcemanager.googleapis.com", #required for SA impersonification
"iam.googleapis.com", #required for IAM and SA impersonification
"cloudfunctions.googleapis.com", #required for Cloud Functions
"pubsub.googleapis.com", #required for PubSub
"billingbudgets.googleapis.com", #required for Billing alerting,
"run.googleapis.com", #required for Cloud Functions 2nd gen
"cloudbuild.googleapis.com", #required for Cloud Functions 2nd gen
"eventarc.googleapis.com", #required for Cloud Functions 2nd gen
"vpcaccess.googleapis.com", #required for Serverless VPC access
]
iam = {
"roles/owner" = [
local.service_accounts.project_factory_test,
]
}
group_iam = {
"foundationreply@d4science.org" = [
"roles/owner"
]
}
}
module "d4science-test-budget-alert-topic" {
source = "../assets/modules-fabric/v26/pubsub"
project_id = module.d4science-test-budget-script-project.project_id
name = "d4science-test-budget-alerting-topic"
}
# module "d4science-test-billing-budget-overall" {
# source = "../assets/modules-custom/billing-budget"
# billing_account = local.billing_account_id
# name = "Test - Overall budget"
# amount = 1000
# currency_code = "EUR"
# thresholds = {
# current = [0.5, 0.75, 1.0]
# forecasted = []
# }
# projects = [
# "projects/${local.networking.spoke-test-project.number}",
# "projects/${module.d4science-test-budget-script-project.number}",
# "projects/${module.d4science-test-vre-project.number}"
# ]
# notification_channels = [local.monitoring.channels["budget-alerting"]]
# }
# module "d4science-test-billing-budget-jupyter-hub" {
# source = "../assets/modules-custom/billing-budget"
# billing_account = local.billing_account_id
# name = "Test - GKE - JupyterHub namespace budget"
# amount = 100
# currency_code = "EUR"
# thresholds = {
# current = [0.5, 0.75, 1.0]
# forecasted = []
# }
# labels = {
# "k8s-namespace" = "jupyter-hub"
# }
# resource_ancestors = [local.folders.test.id]
# notification_channels = [local.monitoring.channels["budget-alerting"]]
# pubsub_topic = module.d4science-test-budget-alert-topic.id # Attention: this cannot be done without temporarily deactivating the org policy iam.allowedPolicyMemberDomains
# }
# module "d4science-test-billing-budget-blue-cloud" {
# source = "../assets/modules-custom/billing-budget"
# billing_account = local.billing_account_id
# name = "Test - GKE - BlueCloud namespace budget"
# amount = 100
# currency_code = "EUR"
# thresholds = {
# current = [0.5, 0.75, 1.0]
# forecasted = []
# }
# labels = {
# "k8s-namespace" = "blue-cloud"
# }
# resource_ancestors = [local.folders.test.id]
# notification_channels = [local.monitoring.channels["budget-alerting"]]
# pubsub_topic = module.d4science-test-budget-alert-topic.id # Attention: this cannot be done without temporarily deactivating the org policy iam.allowedPolicyMemberDomains
# }
# module "d4science-test-billing-budget-i-marine" {
# source = "../assets/modules-custom/billing-budget"
# billing_account = local.billing_account_id
# name = "Test - GKE - iMarine namespace budget"
# amount = 100
# currency_code = "EUR"
# thresholds = {
# current = [0.5, 0.75, 1.0]
# forecasted = []
# }
# labels = {
# "k8s-namespace" = "i-marine"
# }
# resource_ancestors = [local.folders.test.id]
# notification_channels = [local.monitoring.channels["budget-alerting"]]
# pubsub_topic = module.d4science-test-budget-alert-topic.id # Attention: this cannot be done without temporarily deactivating the org policy iam.allowedPolicyMemberDomains
# }
# module "d4science-test-billing-budget-so-big-data" {
# source = "../assets/modules-custom/billing-budget"
# billing_account = local.billing_account_id
# name = "Test - GKE - SoBigData namespace budget"
# amount = 100
# currency_code = "EUR"
# thresholds = {
# current = [0.5, 0.75, 1.0]
# forecasted = []
# }
# labels = {
# "k8s-namespace" = "so-big-data"
# }
# resource_ancestors = [local.folders.test.id]
# notification_channels = [local.monitoring.channels["budget-alerting"]]
# pubsub_topic = module.d4science-test-budget-alert-topic.id # Attention: this cannot be done without temporarily deactivating the org policy iam.allowedPolicyMemberDomains
# }
# module "d4science-test-billing-budget-open-community" {
# source = "../assets/modules-custom/billing-budget"
# billing_account = local.billing_account_id
# name = "Test - GKE - OpenCommunity namespace budget"
# amount = 100
# currency_code = "EUR"
# thresholds = {
# current = [0.5, 0.75, 1.0]
# forecasted = []
# }
# labels = {
# "k8s-namespace" = "open-community"
# }
# resource_ancestors = [local.folders.test.id]
# notification_channels = [local.monitoring.channels["budget-alerting"]]
# pubsub_topic = module.d4science-test-budget-alert-topic.id # Attention: this cannot be done without temporarily deactivating the org policy iam.allowedPolicyMemberDomains
# }
module "d4science-test-budget-script-sa" {
source = "../assets/modules-fabric/v26/iam-service-account"
project_id = module.d4science-test-budget-script-project.project_id
name = "d4science-test-budget-cf-sa"
iam = {
"roles/iam.serviceAccountTokenCreator" = ["group:foundationreply@d4science.org"]
#Impersonate service accounts (create OAuth2 access tokens, sign blobs or JWTs, etc).
}
iam_billing_roles = {
"${local.billing_account_id}" = ["roles/billing.viewer"]
}
}
resource "google_vpc_access_connector" "d4science-test-budget-script-vpc-connector" {
project = module.d4science-test-budget-script-project.project_id
name = "d4science-test-cf-vpc-con"
region = var.gke_region
subnet {
name = "d4science-test-ew8-vpc-con-sub"
project_id = local.networking.spoke-test-project.project_id
}
}

View File

@ -0,0 +1,13 @@
gke_region = "europe-west8"
gke_subnet_id = "europe-west8/d4science-test-ew8-vre-sub"
gke_master_auth_networks = {
"cnr-d4science-1" = "146.48.28.0/22",
"cnr-d4science-2" = "146.48.122.0/23",
"cnr-d4science-3" = "145.90.225.224/27",
# "cnr-d4science-4" = "2001:610:450:80::/64",
"reply-vpn-1" = "91.218.224.5/32",
"reply-vpn-2" = "91.218.224.15/32",
"reply-vpn-3" = "91.218.226.5/32",
"reply-vpn-4" = "91.218.226.15/32",
"budget-cf-vpc-access" = "10.254.66.0/28"
}

View File

@ -0,0 +1,23 @@
variable "outputs_location" {
description = "Assets path location relative to the module"
type = string
default = "../assets"
}
variable "gke_subnet_id" {
description = "Id of the subnet where gke cluster must reside"
type = string
default = ""
}
variable "gke_region" {
description = "Region where gke cluster is located"
type = string
default = ""
}
variable "gke_master_auth_networks" {
description = "Networks which are allowed to access gke control plane"
type = map(string)
default = {}
}

View File

@ -0,0 +1,88 @@
module "d4science-test-vre-gke-cluster" {
source = "../assets/modules-fabric/v26/gke-cluster-standard"
project_id = module.d4science-test-vre-project.project_id
name = "d4science-test-vre-gke-cluster"
location = var.gke_region
vpc_config = {
network = local.networking.spoke-test-project.network
subnetwork = local.networking.spoke-test-project.subnets[var.gke_subnet_id].self_link
secondary_range_names = {
pods = "pods"
services = "services"
}
master_authorized_ranges = var.gke_master_auth_networks
master_ipv4_cidr_block = "10.249.0.64/28"
}
private_cluster_config = {
enable_private_endpoint = false
master_global_access = false
}
enable_features = {
dataplane_v2 = true
workload_identity = true
cost_management = true
}
enable_addons = {
gce_persistent_disk_csi_driver = true
gcp_filestore_csi_driver = true
horizontal_pod_autoscaling = true
http_load_balancing = true
}
logging_config = {
enable_workloads_logs = true
enable_api_server_logs = true
enable_scheduler_logs = true
enable_controller_manager_logs = true
}
monitoring_config = {
enable_api_server_metrics = true
enable_controller_manager_metrics = true
enable_scheduler_metrics = true
enable_daemonset_metrics = true
enable_deployment_metrics = true
enable_hpa_metrics = true
enable_pod_metrics = true
enable_statefulset_metrics = true
enable_storage_metrics = true
}
labels = {
environment = "test"
}
}
module "d4science-test-vre-gke-nodepool" {
source = "../assets/modules-fabric/v26/gke-nodepool"
project_id = module.d4science-test-vre-project.project_id
cluster_name = module.d4science-test-vre-gke-cluster.name
location = var.gke_region
name = "d4science-test-vre-gke-nodepool"
service_account = {
create = true
email = "d4science-test-vre-nodepoolsa"
oauth_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
}
node_config = {
machine_type = "e2-medium"
gcfs = true
}
nodepool_config = {
autoscaling = {
max_node_count = 2
min_node_count = 0
}
management = {
auto_repair = true
auto_upgrade = true
}
}
}
module "d4science_test_artifact_registry" {
source = "../assets/modules-fabric/v26/artifact-registry"
project_id = module.d4science-test-vre-project.project_id
location = var.gke_region
name = "d4science-test-images"
iam = {
"roles/artifactregistry.reader" = [module.d4science-test-vre-gke-nodepool.service_account_iam_email]
}
}

View File

@ -0,0 +1,105 @@
module "d4science-test-vre-project" {
source = "../assets/modules-fabric/v26/project"
name = "d4science-test-vre-prj"
prefix = local.prefix
billing_account = local.billing_account_id
parent = local.folders.test.id
labels = local.labels
auto_create_network = false
project_create = true
services = [
"cloudresourcemanager.googleapis.com", #required for SA impersonification
"iam.googleapis.com", #required for IAM and SA impersonification
"container.googleapis.com", #required for GKE
"artifactregistry.googleapis.com", #required for Artifact registry
"pubsub.googleapis.com", #required for PubSub
"billingbudgets.googleapis.com", #required for Billing alerting
"certificatemanager.googleapis.com", #required for certificates
"secretmanager.googleapis.com", #required for secrets
"logging.googleapis.com", #required for logging
"file.googleapis.com", #required for Filestore
"servicenetworking.googleapis.com",
"containerfilesystem.googleapis.com" #required for image streaming in GKE
]
iam = {
"roles/owner" = [
local.service_accounts.project_factory_test,
]
"roles/container.developer" = ["serviceAccount:${module.d4science-test-budget-script-sa.email}"]
}
group_iam = {
"foundationreply@d4science.org" = [
"roles/owner"
]
}
logging_sinks = {
gke = {
destination = local.monitoring.bucket-sink.id
filter = "resource.type=\"k8s_cluster\""
type = "logging"
unique_writer = true
}
}
}
module "d4science-test-vre-addresses" {
source = "../assets/modules-fabric/v26/net-address"
project_id = module.d4science-test-vre-project.project_id
global_addresses = ["d4science-test-vre-address-ext"]
}
module "d4science-test-vre-ssl-secret" {
source = "../assets/modules-fabric/v26/secret-manager"
project_id = module.d4science-test-vre-project.project_id
secrets = {
# Must be uploaded manually
d4science-test-vre-ssl-private-key = ["europe-west8"],
d4science-test-vre-ssl-public-cert = ["europe-west8"]
}
}
resource "google_compute_ssl_policy" "d4science-test-vre-ssl-policy-1_2-modern" {
project = module.d4science-test-vre-project.project_id
name = "d4science-test-ssl-policy-1-2-modern"
profile = "MODERN"
min_tls_version = "TLS_1_2"
}
resource "google_compute_security_policy" "d4science-test-vre-armor-policy" {
project = module.d4science-test-vre-project.project_id
name = "d4science-test-vre-armor-policy"
adaptive_protection_config {
layer_7_ddos_defense_config {
enable = true
}
}
rule {
action = "deny(403)"
description = "Block RU"
priority = "1000"
match {
expr {
expression = "origin.region_code == 'RU'"
}
}
}
rule {
action = "allow"
priority = "2147483647"
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["*"]
}
}
description = "default rule"
}
}

76
README.md Normal file
View File

@ -0,0 +1,76 @@
gcloud auth application-default login
## D4Science Foundation
This repository contains all the folder structure for D4Science foundation to support VRE applications.
### PRJ Details
- Billing account: `018258-BCA804-E9D4C6`
- Tenant:
- `d4science`
## Folder structure
```
.
├── 00-organization
├── 01-networking
│   └── data
│   └── subnets
├── 02-security
│   └── firewall
│   └── rules
│   ├── 00-spoke-test
│   ├── 01-spoke-prod
├── 03-monitoring
│   └── data
├── 04-project-factory-test
├── 04-project-factory-prod
└── assets
├── providers
└── tfvars
```
## Naming Convention
The naming convention for this foundation project is the following one:
`<tenant>-<env>-<name>-prj`
- tenant: `d4science`
- env:
- test
- prod
- name: project name
### Resources
The naming convention to use for the resources of this project is:
`<tenant>-<env>-<location (optional)-<app. initiative (optional)>-<unique id>-<res. short name>`
#### Resource name examples
- Service Account: `d4science-dev-foundation-tfnet-sa`
- VPC: `d4science-dev-glb-spoke-vpc` (location glb = global)
- Subnet: `d4science-dev-ew8-01-sub` (unique id = 01 used as incremental counter)
- Bucket: `d4science-org-ew8-foundation-tforg-bkt`
- peering: `d4science-prod-hubspoke-peer`
### Firewall Rule
The naming convention to use for the firewall rules of this project is:
`<tenant>-<env>-glb-<app. initiative>-<ing/egr>-<alw/den>-<unique id>-<res. short name>`
#### FW rule example
- FW rule: `d4science-test-glb-paytas-ing-alw-gke-fwr`
### General considerations
- Avoid fields with "-"
- unique id can be:
- numbers (e.g. 01)
- letters (e.g. abc)
- both (e.g. abc01)
### SHORT NAMES
#### Resources
- SA: service account
- BKT: bucket
- PRJ: project
- SUB: subnet
- VPC: vpc
- FWR: Firewall

View File

@ -0,0 +1,89 @@
# Google Cloud Billing Budget Module
This module allows creating a Cloud Billing budget for a set of services and projects.
To create billing budgets you need one of the following IAM roles on the target billing account:
* Billing Account Administrator
* Billing Account Costs Manager
## Examples
### Simple email notification
Send a notification to an email when a set of projects reach $100 of spend.
```hcl
module "budget" {
source = "./fabric/modules/billing-budget"
billing_account = var.billing_account_id
name = "$100 budget"
amount = 100
thresholds = {
current = [0.5, 0.75, 1.0]
forecasted = [1.0]
}
projects = [
"projects/123456789000",
"projects/123456789111"
]
email_recipients = {
project_id = "my-project"
emails = ["user@example.com"]
}
}
# tftest modules=1 resources=2 inventory=email.yaml
```
### Pubsub notification
Send a notification to a PubSub topic the total spend of a billing account reaches the previous month's spend.
```hcl
module "budget" {
source = "./fabric/modules/billing-budget"
billing_account = var.billing_account_id
name = "previous period budget"
amount = 0
thresholds = {
current = [1.0]
forecasted = []
}
pubsub_topic = module.pubsub.id
}
module "pubsub" {
source = "./fabric/modules/pubsub"
project_id = var.project_id
name = "budget-topic"
}
# tftest modules=2 resources=2 inventory=pubsub.yaml
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [billing_account](variables.tf#L23) | Billing account id. | <code>string</code> | ✓ | |
| [name](variables.tf#L50) | Budget name. | <code>string</code> | ✓ | |
| [thresholds](variables.tf#L85) | Thresholds percentages at which alerts are sent. Must be a value between 0 and 1. | <code title="object&#40;&#123;&#10; current &#61; list&#40;number&#41;&#10; forecasted &#61; list&#40;number&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [amount](variables.tf#L17) | Amount in the billing account's currency for the budget. Use 0 to set budget to 100% of last period's spend. | <code>number</code> | | <code>0</code> |
| [credit_treatment](variables.tf#L28) | How credits should be treated when determining spend for threshold calculations. Only INCLUDE_ALL_CREDITS or EXCLUDE_ALL_CREDITS are supported. | <code>string</code> | | <code>&#34;INCLUDE_ALL_CREDITS&#34;</code> |
| [email_recipients](variables.tf#L41) | Emails where budget notifications will be sent. Setting this will create a notification channel for each email in the specified project. | <code title="object&#40;&#123;&#10; project_id &#61; string&#10; emails &#61; list&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [notification_channels](variables.tf#L55) | Monitoring notification channels where to send updates. | <code>list&#40;string&#41;</code> | | <code>null</code> |
| [notify_default_recipients](variables.tf#L61) | Notify Billing Account Administrators and Billing Account Users IAM roles for the target account. | <code>bool</code> | | <code>false</code> |
| [projects](variables.tf#L67) | List of projects of the form projects/{project_number}, specifying that usage from only this set of projects should be included in the budget. Set to null to include all projects linked to the billing account. | <code>list&#40;string&#41;</code> | | <code>null</code> |
| [pubsub_topic](variables.tf#L73) | The ID of the Cloud Pub/Sub topic where budget related messages will be published. | <code>string</code> | | <code>null</code> |
| [services](variables.tf#L79) | List of services of the form services/{service_id}, specifying that usage from only this set of services should be included in the budget. Set to null to include usage for all services. | <code>list&#40;string&#41;</code> | | <code>null</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [budget](outputs.tf#L17) | Budget resource. | |
| [id](outputs.tf#L22) | Fully qualified budget id. | |
<!-- END TFDOC -->

View File

@ -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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 {
spend_basis = {
current = "CURRENT_SPEND"
forecasted = "FORECASTED_SPEND"
}
threshold_pairs = flatten([
for type, values in var.thresholds : [
for value in values : {
spend_basis = local.spend_basis[type]
threshold_percent = value
}
]
])
notification_channels = concat(
[for channel in google_monitoring_notification_channel.email_channels : channel.id],
coalesce(var.notification_channels, [])
)
}
resource "google_monitoring_notification_channel" "email_channels" {
for_each = toset(try(var.email_recipients.emails, []))
display_name = "${var.name} budget email notification (${each.value})"
type = "email"
project = var.email_recipients.project_id
labels = {
email_address = each.value
}
user_labels = {}
}
resource "google_billing_budget" "budget" {
billing_account = var.billing_account
display_name = var.name
budget_filter {
projects = var.projects
resource_ancestors = var.resource_ancestors
credit_types_treatment = var.credit_treatment
services = var.services
labels = var.labels
calendar_period = var.calendar_period
}
dynamic "amount" {
for_each = var.amount == 0 ? [1] : []
content {
last_period_amount = true
}
}
dynamic "amount" {
for_each = var.amount != 0 ? [1] : []
content {
dynamic "specified_amount" {
for_each = var.amount != 0 ? [1] : []
content {
units = var.amount
currency_code = var.currency_code
}
}
}
}
dynamic "threshold_rules" {
for_each = local.threshold_pairs
iterator = threshold
content {
threshold_percent = threshold.value.threshold_percent
spend_basis = threshold.value.spend_basis
}
}
all_updates_rule {
monitoring_notification_channels = local.notification_channels
pubsub_topic = var.pubsub_topic
# disable_default_iam_recipients can only be set if
# monitoring_notification_channels is nonempty
disable_default_iam_recipients = try(length(var.notification_channels), 0) > 0 && !var.notify_default_recipients
schema_version = "1.0"
}
}

View File

@ -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.
*/
output "budget" {
description = "Budget resource."
value = google_billing_budget.budget
}
output "id" {
description = "Fully qualified budget id."
value = google_billing_budget.budget.id
}

View File

@ -0,0 +1,123 @@
/**
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "amount" {
description = "Amount in the billing account's currency for the budget. Use 0 to set budget to 100% of last period's spend."
type = number
default = 0
}
variable "currency_code" {
description = "The 3-letter currency code defined in ISO 4217."
type = string
default = "USD"
}
variable "billing_account" {
description = "Billing account id."
type = string
}
variable "credit_treatment" {
description = "How credits should be treated when determining spend for threshold calculations. Only INCLUDE_ALL_CREDITS or EXCLUDE_ALL_CREDITS are supported."
type = string
default = "INCLUDE_ALL_CREDITS"
validation {
condition = (
var.credit_treatment == "INCLUDE_ALL_CREDITS" ||
var.credit_treatment == "EXCLUDE_ALL_CREDITS"
)
error_message = "Argument credit_treatment must be INCLUDE_ALL_CREDITS or EXCLUDE_ALL_CREDITS."
}
}
variable "email_recipients" {
description = "Emails where budget notifications will be sent. Setting this will create a notification channel for each email in the specified project."
type = object({
project_id = string
emails = list(string)
})
default = null
}
variable "name" {
description = "Budget name."
type = string
}
variable "notification_channels" {
description = "Monitoring notification channels where to send updates."
type = list(string)
default = null
}
variable "notify_default_recipients" {
description = "Notify Billing Account Administrators and Billing Account Users IAM roles for the target account."
type = bool
default = false
}
variable "projects" {
description = "List of projects of the form projects/{project_number}, specifying that usage from only this set of projects should be included in the budget. Set to null to include all projects linked to the billing account."
type = list(string)
default = null
}
variable "resource_ancestors" {
description = "List of organizations of the form organizations/{organization_id} and/or folders of the form folders/{folder_id}, specifying that usage from only this set of ancestors should be included in the budget. Set to null to include all organizations and folders linked to the billing account."
type = list(string)
default = null
}
variable "pubsub_topic" {
description = "The ID of the Cloud Pub/Sub topic where budget related messages will be published."
type = string
default = null
}
variable "services" {
description = "List of services of the form services/{service_id}, specifying that usage from only this set of services should be included in the budget. Set to null to include usage for all services."
type = list(string)
default = null
}
variable "labels" {
description = "A single label and value pair specifying that usage from only this set of labeled resources should be included in the budget. Set to null to include all labels."
type = map(string)
default = null
}
variable "thresholds" {
description = "Thresholds percentages at which alerts are sent. Must be a value between 0 and 1."
type = object({
current = list(number)
forecasted = list(number)
})
validation {
condition = length(var.thresholds.current) > 0 || length(var.thresholds.forecasted) > 0
error_message = "Must specify at least one budget threshold."
}
}
variable "calendar_period" {
description = "The calendar period the budget spans. It can be either MONTH, QUARTER or YEAR."
type = string
default = null
validation {
condition = var.calendar_period == null || (var.calendar_period == null ? true : contains(["MONTH", "QUARTER", "YEAR"], var.calendar_period))
error_message = "Allowed values for calendar_period are \"MONTH\", \"QUARTER\", or \"YEAR\"."
}
}

View File

@ -0,0 +1,29 @@
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# 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.4.4"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 4.82.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
version = ">= 4.82.0" # tftest
}
}
}

View File

@ -0,0 +1,107 @@
#-------------------------------------------------------------------------------
# DNS Zones for PRIVATE GOOGLE ACCESS
#-------------------------------------------------------------------------------
/* !!!!! README !!!!!
* We use restricted.googleapis.com and additional domains linked to restricted APIS.
* Private Google Apis (private.googleapis.com) has been left here for future use, if needed.
*/
locals {
private_ips = ["199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"]
restricted_ips = ["199.36.153.4", "199.36.153.5", "199.36.153.6", "199.36.153.7"]
ips = var.config.private ? local.private_ips : local.restricted_ips
name = var.config.private ? "private" : "restricted"
fixed_domains = {
accounts = ["accounts.google.com"]
appengine = ["appengine.google.com"]
appspot = ["*.appspot.com"]
artifact = ["*.gcr.io", "*.pkg.dev"]
functions = ["*.cloudfunctions.net"]
proxy = ["*.cloudproxy.app"]
run = ["*.run.app"]
composer = ["*.composer.cloud.google.com", "*.composer.googleusercontent.com"]
datafusion = ["*.datafusion.cloud.google.com", "*.datafusion.googleusercontent.com"]
dataproc = ["*.dataproc.cloud.google.com", "dataproc.cloud.google.com", "*.dataproc.googleusercontent.com", "dataproc.googleusercontent.com"]
download = ["dl.google.com"]
ad = ["*.googleadapis.com"]
gstatic = ["*.gstatic.com"]
lts = ["*.ltsapis.goog"]
notebooks = ["*.notebooks.cloud.google.com", "*.notebooks.googleusercontent.com"]
packages = ["packages.cloud.google.com"]
pki = ["*.pki.goog"]
source = ["source.developers.google.com"]
}
enabled_fixed_domain = merge([
for k, v in var.domains : {
for i, d in local.fixed_domains[k] :
"${k}-${i}" => d
} if v
]...)
enabled_custom_domain = merge([
for k, v in var.custom_domains : {
for i, d in v :
"${k}-${i}" => d
}
]...)
enabled_domain = merge(local.enabled_fixed_domain, local.enabled_custom_domain)
}
#-------------------------------------
# PRIVATE.GOOGLEAPIS.COM
#-------------------------------------
# DNS zone for PGA: *.googleapis.com.
module "googleapis_zone" {
source = "../../modules-fabric/v26/dns"
project_id = var.project_id
name = "${var.name}-pga-${local.name}-apis"
zone_config = {
domain = "googleapis.com."
private = {
client_networks = var.networks
}
}
recordsets = {
"A ${local.name}.googleapis.com." = { ttl = 300, records = local.ips }
"CNAME *.googleapis.com." = { ttl = 300, records = ["${local.name}.googleapis.com."] }
}
}
# DNS zone for others APIs
# All others api (different from *.googleapis.com)
module "pga_apis_zone" {
for_each = local.enabled_domain
source = "../../modules-fabric/v26/dns"
project_id = var.project_id
name = "${var.name}-pga-${local.name}-api-${each.key}"
zone_config = {
domain = "${trimprefix(each.value, "*.")}."
private = {
client_networks = var.networks
}
}
recordsets = {
# We are adding wildcard even if not needed, as per documentation.
"A ${trimprefix(each.value, "*.")}." = { ttl = 300, records = local.ips }
"CNAME *.${trimprefix(each.value, "*.")}." = { ttl = 300, records = ["${trimprefix(each.value, "*.")}."] }
}
}
resource "google_service_networking_peered_dns_domain" "apis-dns-peering" {
for_each = can(var.config.value["service_networking_network"]) ? local.enabled_domain : {}
project = var.project_id
network = var.config.service_networking_network
name = "${replace(trimprefix(each.value, "*."), ".", "-")}-domain"
dns_suffix = "*.${trimprefix(each.value, "*.")}."
}
resource "google_service_networking_peered_dns_domain" "googleapis-dns-peering" {
count = can(var.config.value["service_networking_network"]) ? 1 : 0
project = var.project_id
name = "googleapis-domain"
network = var.config.service_networking_network
dns_suffix = "googleapis."
}

View File

View File

@ -0,0 +1,59 @@
variable "domains" {
description = "Declare which domains to redirect to PGA"
type = object({
accounts = optional(bool, false)
appengine = optional(bool, false)
appspot = optional(bool, false)
artifact = optional(bool, false)
functions = optional(bool, false)
proxy = optional(bool, false)
run = optional(bool, false)
composer = optional(bool, false)
datafusion = optional(bool, false)
dataproc = optional(bool, false)
download = optional(bool, false)
ad = optional(bool, false)
gstatic = optional(bool, false)
lts = optional(bool, false)
notebooks = optional(bool, false)
packages = optional(bool, false)
pki = optional(bool, false)
source = optional(bool, false)
})
default = {}
}
variable "custom_domains" {
type = map(list(string))
description = "Optional map {NAME}=>[{domain1},{domain2},...]"
default = {}
}
variable "config" {
type = object({
private = optional(bool, false)
restricted = optional(bool, false)
service_networking_network = optional(string)
})
description = "(optional) describe your variable"
validation {
condition = var.config.private != var.config.restricted
error_message = "One and only one between private and restricted must be set to true"
}
}
variable "project_id" {
type = string
description = "DNS zone project id"
}
variable "name" {
type = string
description = "Name for the created resources"
}
variable "networks" {
type = list(string)
description = "List of networks self-links for DNS zone"
}

View File

@ -0,0 +1,109 @@
# Terraform modules suite for Google Cloud
The modules collected in this folder are designed as a suite: they are meant to be composed together, and are designed to be forked and modified where use of third party code and sources is not allowed.
Modules try to stay close to the low level provider resources they encapsulate, and they all share a similar interface that combines management of one resource or set or resources, and the corresponding IAM bindings.
Authoritative IAM bindings are primarily used (e.g. `google_storage_bucket_iam_binding` for GCS buckets) so that each module is authoritative for specific roles on the resources it manages, and can neutralize or reconcile IAM changes made elsewhere.
Specific modules also offer support for non-authoritative bindings (e.g. `google_storage_bucket_iam_member` for service accounts), to allow granular permission management on resources that they don't manage directly.
These modules are not necessarily backward compatible. Changes breaking compatibility in modules are marked by major releases (but not all major releases contain breaking changes). Please be mindful when upgrading Fabric modules in existing Terraform setups, and always try to use versioned references in module sources so you can easily revert back to a previous version. Since the introduction of the `moved` block in Terraform we try to use it whenever possible to make updates non-breaking, but that does not cover all changes we might need to make.
These modules are used in the examples included in this repository. If you are using any of those examples in your own Terraform configuration, make sure that you are using the same version for all the modules, and switch module sources to GitHub format using references. The recommended approach to working with Fabric modules is the following:
- Fork the repository and own the fork. This will allow you to:
- Evolve the existing modules.
- Create your own modules.
- Sync from the upstream repository to get all the updates.
- Use GitHub sources with refs to reference the modules. See an example below:
```terraform
module "project" {
source = "../assets/modules-fabric/v26/project"
name = "my-project"
billing_account = "123456-123456-123456"
parent = "organizations/123456"
}
```
## Foundational modules
- [billing budget](./billing-budget)
- [Cloud Identity group](./cloud-identity-group/)
- [folder](./folder)
- [service accounts](./iam-service-account)
- [logging bucket](./logging-bucket)
- [organization](./organization)
- [project](./project)
- [projects-data-source](./projects-data-source)
## Networking modules
- [Address reservation](./net-address)
- [Cloud Endpoints](./endpoints)
- [DNS](./dns)
- [DNS Response Policy](./dns-response-policy/)
- [Firewall policy](./net-firewall-policy)
- [External Application Load Balancer](./net-lb-app-ext/)
- [External Passthrough Network Load Balancer](./net-lb-ext)
- [Internal Application Load Balancer](./net-lb-app-int)
- [Internal Passthrough Network Load Balancer](./net-lb-int)
- [Internal Proxy Network Load Balancer](./net-lb-proxy-int)
- [Internal ]
- [NAT](./net-cloudnat)
- [Service Directory](./service-directory)
- [VPC](./net-vpc)
- [VPC firewall](./net-vpc-firewall)
- [VPN dynamic](./net-vpn-dynamic)
- [VPC peering](./net-vpc-peering)
- [VPN HA](./net-vpn-ha)
- [VPN static](./net-vpn-static)
## Compute/Container
- [VM/VM group](./compute-vm)
- [MIG](./compute-mig)
- [COS container](./cloud-config-container/cos-generic-metadata/) (coredns/mysql/nva/onprem/squid)
- [GKE autopilot cluster](./gke-cluster-autopilot)
- [GKE standard cluster](./gke-cluster-standard)
- [GKE hub](./gke-hub)
- [GKE nodepool](./gke-nodepool)
- [GCVE private cloud](./gcve-private-cloud)
## Data
- [AlloyDB instance](./alloydb-instance)
- [BigQuery dataset](./bigquery-dataset)
- [Bigtable instance](./bigtable-instance)
- [Dataplex](./dataplex)
- [Dataplex DataScan](./dataplex-datascan/)
- [Cloud SQL instance](./cloudsql-instance)
- [Data Catalog Policy Tag](./data-catalog-policy-tag)
- [Datafusion](./datafusion)
- [Dataproc](./dataproc)
- [GCS](./gcs)
- [Pub/Sub](./pubsub)
## Development
- [API Gateway](./api-gateway)
- [Apigee](./apigee)
- [Artifact Registry](./artifact-registry)
- [Container Registry](./container-registry)
- [Cloud Source Repository](./source-repository)
## Security
- [Binauthz](./binauthz/)
- [KMS](./kms)
- [SecretManager](./secret-manager)
- [VPC Service Control](./vpc-sc)
- [Secure Web Proxy](./net-swp)
## Serverless
- [Cloud Functions v1](./cloud-function-v1)
- [Cloud Functions v2](./cloud-function-v2)
- [Cloud Run](./cloud-run)

View File

@ -0,0 +1,344 @@
# Refactor IAM interface
**authors:** [Ludo](https://github.com/ludoo), [Julio](https://github.com/juliocc)
**last modified:** August 17, 2023
## Status
Implemented in [#1595](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1595).
Authoritative bindings type changed as per [#1622](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/issues/1622).
## Context
The IAM interface in our modules has evolved organically to progressively support more functionality, resulting in a large variable surface, lack of support for some key features like conditions, and some fragility for specific use cases.
We currently support, with uneven coverage across modules:
- authoritative `iam` in `ROLE => [PRINCIPALS]` format
- authoritative `group_iam` in `GROUP => [ROLES]` format
- legacy additive `iam_additive` in `ROLE => [PRINCIPALS]` format which breaks for dynamic values
- legacy additive `iam_additive_members` in `PRINCIPAL => [ROLES]` format which breaks for dynamic values
- new additive `iam_members` in `KEY => {role: ROLE, member: MEMBER, condition: CONDITION}` format which works with dynamic values and supports conditions
- policy authoritative `iam_policy`
- specific support for third party resource bindings in the service account module
## Proposal
### Authoritative bindings
These tend to work well in practice, and the current `iam` and `group_iam` variables are simple to use with good coverage across modules.
The only small use case that they do not cover is IAM conditions, which are easy to implement but would render the interface more verbose for the majority of cases where conditions are not needed.
The **proposal** for authoritative bindings is to
- leave the current interface in place (`iam` and `group_iam`)
- expand coverage so that all modules who have iam resources expose both
- add a new `iam_bindings` variable to support authoritative IAM with conditions
The new `iam_bindings` variable will look like this:
```hcl
variable "iam_bindings" {
description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
type = map(object({
members = list(string)
role = string
condition = optional(object({
expression = string
title = string
description = optional(string)
}))
}))
nullable = false
default = {}
}
```
This variable will not be internally merged in modules with `iam` or `group_iam`.
### Additive bindings
Additive bindings have evolved to mimick authoritative ones, but the result is an interface which is bloated (no one uses `iam_additive_members`), and hard to understand and use without triggering dynamic errors. Coverage is also spotty and uneven across modules, and the interface needs to support aliasing of project service accounts in the project module to work around dynamic errors.
The `iam_additive` variable is used in a special patterns in data blueprints, to allow code to not mess up existing IAM bindings in an external project on destroy. This pattern only works in a limited set of cases, where principals are passed in via static variables or refer to "magic" static outputs in our modules. This is a simple example of the pattern:
```hcl
locals {
iam = {
"roles/viewer" = [
module.sa.iam_email,
var.group.admins
]
}
}
module "project" {
iam = (
var.project_create == null ? {} : local.iam
)
iam_additive = (
var.project_create != null ? {} : local.iam
)
}
```
The **proposal** for authoritative bindings is to
- remove `iam_additive` and `iam_additive_members` from the interface
- add a new `iam_bindings_additive` variable
Once new variables are in place, migrate existing blueprints to using `iam_bindings_additive` using one of the two available patterns:
- the flat verbose one where bindings are declared in the module call
- the more complex one that moves roles out to `locals` and uses them in `for` loops
The new variable will closely follow the type of the authoritative `iam_bindings` variable described above:
```hcl
variable "iam_bindings_additive" {
description = "Additive IAM bindings with support for conditions, in {KEY => { role = ROLE, members = [], condition = {}}} format."
type = map(object({
member = string
role = string
condition = optional(object({
expression = string
title = string
description = optional(string)
}))
}))
}
```
### IAM policy
The **proposal** is to remove the IAM policy variable and resources, as its coverage is very uneven and we never used it in practice. This will also simplify data access log management, which is currently split between its own variable/resource and the IAM policy ones.
## Decision
The proposal above summarizes the state of discussions between the authors, and implementation will be tested.
## Consequences
### FAST
IAM implementation in the bootstrap stage and matching multitenant bootstrap has radically changed, with the addition of a new [`organization-iam.tf`](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/blob/master/fast/stages/0-bootstrap/organization-iam.tf) file which contains IAM binding definitions in an abstracted format, that is then converted to the specific formats required by the `iam`, `iam_bindings` and `iam_bindings_additive` variables.
This brings several advantages over the previous handling of IAM:
- authoritative and additive bindings are now grouped by principal in an easy to read and change format that serves as its own documentation
- support for IAM conditions has removed the need for standalone resources and made the intent behind those more explicit
- some subtle bugs on the intersection of user-specified bindings and internally-specified ones have been addressed
### Blueprints
A few data blueprints that leverage `iam_additive` have been refactored to use the new variable. This is most notable in data blueprints, where extra files have been added to the more complex examples like data foundations, to abstract IAM bindings in a way similar to what is described above for FAST.
## Implementation
The following sections provide a template for IAM-related variables and resources to ensure a consistent implementation of IAM across the repository. Use these code snippets to add IAM support to your module.
### Top-level module IAM
Use this template if your module manages a single instance of a given resource (e.g. a KMS keyring).
```terraform
# variables.tf
variable "iam" {
description = "IAM bindings in {ROLE => [MEMBERS]} format. Mutually exclusive with the access_* variables used for basic roles."
type = map(list(string))
default = {}
nullable = false
}
variable "iam_bindings" {
description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
type = map(object({
members = list(string)
role = string
condition = optional(object({
expression = string
title = string
description = optional(string)
}))
}))
default = {}
nullable = false
}
variable "iam_bindings_additive" {
description = "Keyring individual additive IAM bindings. Keys are arbitrary."
type = map(object({
member = string
role = string
condition = optional(object({
expression = string
title = string
description = optional(string)
}))
}))
default = {}
nullable = false
}
```
```terraform
# iam.tf
resource "google_RESOURCE_TYPE_iam_binding" "authoritative" {
for_each = var.iam
role = each.key
members = each.value
// add extra attributes (e.g. resource id)
}
resource "google_RESOURCE_TYPE_iam_binding" "bindings" {
for_each = var.iam_bindings
role = each.value.role
members = each.value.members
// add extra attributes (e.g. resource id)
dynamic "condition" {
for_each = each.value.condition == null ? [] : [""]
content {
expression = each.value.condition.expression
title = each.value.condition.title
description = each.value.condition.description
}
}
}
resource "google_RESOURCE_TYPE_iam_member" "bindings" {
for_each = var.iam_bindings_additive
role = each.value.role
member = each.value.member
// add extra attributes (e.g. resource id)
dynamic "condition" {
for_each = each.value.condition == null ? [] : [""]
content {
expression = each.value.condition.expression
title = each.value.condition.title
description = each.value.condition.description
}
}
}
```
### Sub-resources IAM
Use this template if your module manages multiple instances of a resource (e.g. keys in KMS keyring).
```terraform
# variables.tf
variable "sub_resources" {
type = map(object({
# sub-resource configuration here
iam = optional(map(list(string)), {})
iam_bindings = optional(map(object({
members = list(string)
condition = optional(object({
expression = string
title = string
description = optional(string)
}))
})), {})
iam_bindings_additive = optional(map(object({
member = string
role = string
condition = optional(object({
expression = string
title = string
description = optional(string)
}))
})), {})
}))
default = {}
nullable = false
}
```
```terraform
# iam.tf
locals {
SUB_RESOURCE_iam = flatten([
for k, v in var.SUB_RESOURCEs : [
for role, members in v.iam : {
key = k
role = role
members = members
}
]
])
SUB_RESOURCE_iam_bindings = merge([
for k, v in var.SUB_RESOURCEs : {
for binding_key, data in v.iam_bindings :
binding_key => {
SUB_RESOURCE = k
role = data.role
members = data.members
condition = data.condition
}
}
]...)
SUB_RESOURCE_iam_bindings_additive = merge([
for k, v in var.subresources : {
for binding_key, data in v.iam_bindings_additive :
binding_key => {
SUB_RESOURCE = k
role = data.role
member = data.member
condition = data.condition
}
}
]...)
}
```
```terraform
# iam.tf
resource "google_SUB_RESOURCE_iam_binding" "authoritative" {
for_each = {
for binding in local.SUB_RESOURCE_iam :
"${binding.key}.${binding.role}" => binding
}
role = each.value.role
members = each.value.members
// add extra attributes (e.g. sub resource id)
}
resource "google_SUB_RESOURCE_iam_binding" "bindings" {
for_each = local.SUB_RESOURCE_iam_bindings
role = each.value.role
members = each.value.members
// add extra attributes (e.g. sub resource id)
dynamic "condition" {
for_each = each.value.condition == null ? [] : [""]
content {
expression = each.value.condition.expression
title = each.value.condition.title
description = each.value.condition.description
}
}
}
resource "google_SUB_RESOURCE_iam_member" "members" {
for_each = local.SUB_RESOURCE_iam_bindings_additive
role = each.value.role
member = each.value.member
// add extra attributes (e.g. sub resource id)
dynamic "condition" {
for_each = each.value.condition == null ? [] : [""]
content {
expression = each.value.condition.expression
title = each.value.condition.title
description = each.value.condition.description
}
}
}
```

View File

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

View File

@ -0,0 +1,35 @@
# Google Cloud DNS Inbound Policy Addresses
This module allows discovering the addresses reserved in subnets when [DNS Inbound Policies](https://cloud.google.com/dns/docs/policies) are configured.
Since it's currently impossible to fetch those addresses using a GCP data source (see [this issue](https://github.com/hashicorp/terraform-provider-google/issues/3753) for more details), the workaround used here is to derive the authorization token from the Google provider, and do a direct HTTP call to the Compute API.
## Examples
```hcl
module "dns-policy-addresses" {
source = "./fabric/modules/__experimental/net-dns-policy-addresses"
project_id = "myproject"
regions = ["europe-west1", "europe-west3"]
}
# tftest skip (uses data sources)
```
The output is a map with lists of addresses of type `DNS_RESOLVER` for each region specified in variables.
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [project_id](variables.tf#L17) | Project id. | <code>string</code> | ✓ | |
| [regions](variables.tf#L22) | Regions to fetch addresses from. | <code>list&#40;string&#41;</code> | | <code>&#91;&#34;europe-west1&#34;&#93;</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [addresses](outputs.tf#L24) | DNS inbound policy addresses per region. | |
<!-- END TFDOC -->

View File

@ -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 {
url = format(
"https://content-compute.googleapis.com/compute/v1/projects/%s",
var.project_id
)
}
data "google_client_config" "current" {}
data "http" "addresses" {
for_each = toset(var.regions)
url = "${local.url}/regions/${each.key}/addresses?filter=purpose%20%3D%20%22DNS_RESOLVER%22"
request_headers = {
Authorization = "Bearer ${data.google_client_config.current.access_token}"
}
}

View File

@ -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.
*/
locals {
region_addresses = {
for k, v in data.http.addresses : k => try(jsondecode(v.body), {})
}
}
output "addresses" {
description = "DNS inbound policy addresses per region."
value = {
for k, v in local.region_addresses : k => [
for i in try(v.items, []) : i.address
]
}
}

View File

@ -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 "project_id" {
description = "Project id."
type = string
}
variable "regions" {
description = "Regions to fetch addresses from."
nullable = false
type = list(string)
default = ["europe-west1"]
}

View File

@ -0,0 +1,48 @@
# Network Endpoint Group Module
This modules allows creating zonal network endpoint groups.
Note: this module will integrated into a general-purpose load balancing module in the future.
## Example
```hcl
module "neg" {
source = "./fabric/modules/__experimental/net-neg/"
project_id = "myproject"
name = "myneg"
network = var.vpc.self_link
subnetwork = var.subnet.self_link
zone = "europe-west1-b"
endpoints = [
for instance in module.vm.instances :
{
instance = instance.name
port = 80
ip_address = instance.network_interface[0].network_ip
}
]
}
# tftest skip
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [endpoints](variables.tf#L17) | List of (instance, port, address) of the NEG. | <code title="list&#40;object&#40;&#123;&#10; instance &#61; string&#10; port &#61; number&#10; ip_address &#61; string&#10;&#125;&#41;&#41;">list&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | ✓ | |
| [name](variables.tf#L26) | NEG name. | <code>string</code> | ✓ | |
| [network](variables.tf#L31) | Name or self link of the VPC used for the NEG. Use the self link for Shared VPC. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L36) | NEG project id. | <code>string</code> | ✓ | |
| [subnetwork](variables.tf#L41) | VPC subnetwork name or self link. | <code>string</code> | ✓ | |
| [zone](variables.tf#L46) | NEG zone. | <code>string</code> | ✓ | |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [id](outputs.tf#L17) | Network endpoint group ID. | |
| [self_lnk](outputs.tf#L22) | Network endpoint group self link. | |
| [size](outputs.tf#L27) | Size of the network endpoint group. | |
<!-- END TFDOC -->

View File

@ -0,0 +1,33 @@
/**
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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_compute_network_endpoint_group" "group" {
project = var.project_id
name = var.name
network = var.network
subnetwork = var.subnetwork
zone = var.zone
}
resource "google_compute_network_endpoint" "endpoint" {
for_each = { for endpoint in var.endpoints : endpoint.instance => endpoint }
project = var.project_id
network_endpoint_group = google_compute_network_endpoint_group.group.name
instance = each.value.instance
port = each.value.port
ip_address = each.value.ip_address
zone = var.zone
}

View File

@ -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.
*/
output "id" {
description = "Network endpoint group ID."
value = google_compute_network_endpoint_group.group.name
}
output "self_lnk" {
description = "Network endpoint group self link."
value = google_compute_network_endpoint_group.group.self_link
}
output "size" {
description = "Size of the network endpoint group."
value = google_compute_network_endpoint_group.group.size
}

View File

@ -0,0 +1,49 @@
/**
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "endpoints" {
description = "List of (instance, port, address) of the NEG."
type = list(object({
instance = string
port = number
ip_address = string
}))
}
variable "name" {
description = "NEG name."
type = string
}
variable "network" {
description = "Name or self link of the VPC used for the NEG. Use the self link for Shared VPC."
type = string
}
variable "project_id" {
description = "NEG project id."
type = string
}
variable "subnetwork" {
description = "VPC subnetwork name or self link."
type = string
}
variable "zone" {
description = "NEG zone."
type = string
}

View File

@ -0,0 +1,29 @@
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# 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.4.4"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 4.82.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
version = ">= 4.82.0" # tftest
}
}
}

View File

@ -0,0 +1,88 @@
# AlloyDB cluster and instance with read replicas
This module manages the creation of AlloyDB cluster and configuration with/without automated backup policy, Primary node instance and Read Node Pools.
## Simple example
This example shows how to create Alloydb cluster and instance with multiple read pools in GCP project.
```hcl
module "alloydb" {
source = "./fabric/modules/alloydb-instance"
project_id = "myproject"
cluster_id = "alloydb-cluster-all"
location = "europe-west2"
labels = {}
display_name = ""
initial_user = {
user = "alloydb-cluster-full",
password = "alloydb-cluster-password"
}
network_self_link = "projects/myproject/global/networks/default"
automated_backup_policy = null
primary_instance_config = {
instance_id = "primary-instance-1",
instance_type = "PRIMARY",
machine_cpu_count = 2,
database_flags = {},
display_name = "alloydb-primary-instance"
}
read_pool_instance = [
{
instance_id = "read-instance-1",
display_name = "read-instance-1",
instance_type = "READ_POOL",
node_count = 1,
database_flags = {},
machine_cpu_count = 1
},
{
instance_id = "read-instance-2",
display_name = "read-instance-2",
instance_type = "READ_POOL",
node_count = 1,
database_flags = {},
machine_cpu_count = 1
}
]
}
# tftest modules=1 resources=7
```
## TODO
- [ ] Add IAM support
- [ ] support password in output
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [cluster_id](variables.tf#L35) | The ID of the alloydb cluster. | <code>string</code> | ✓ | |
| [network_self_link](variables.tf#L83) | Network ID where the AlloyDb cluster will be deployed. | <code>string</code> | ✓ | |
| [primary_instance_config](variables.tf#L88) | Primary cluster configuration that supports read and write operations. | <code title="object&#40;&#123;&#10; instance_id &#61; string,&#10; display_name &#61; optional&#40;string&#41;,&#10; database_flags &#61; optional&#40;map&#40;string&#41;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;&#41;&#10; annotations &#61; optional&#40;map&#40;string&#41;&#41;&#10; gce_zone &#61; optional&#40;string&#41;&#10; availability_type &#61; optional&#40;string&#41;&#10; machine_cpu_count &#61; optional&#40;number, 2&#41;,&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [project_id](variables.tf#L110) | The ID of the project in which to provision resources. | <code>string</code> | ✓ | |
| [automated_backup_policy](variables.tf#L17) | The automated backup policy for this cluster. | <code title="object&#40;&#123;&#10; location &#61; optional&#40;string&#41;&#10; backup_window &#61; optional&#40;string&#41;&#10; enabled &#61; optional&#40;bool&#41;&#10; weekly_schedule &#61; optional&#40;object&#40;&#123;&#10; days_of_week &#61; optional&#40;list&#40;string&#41;&#41;&#10; start_times &#61; list&#40;string&#41;&#10; &#125;&#41;&#41;,&#10; quantity_based_retention_count &#61; optional&#40;number&#41;&#10; time_based_retention_count &#61; optional&#40;string&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;&#41;&#10; backup_encryption_key_name &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [display_name](variables.tf#L44) | Human readable display name for the Alloy DB Cluster. | <code>string</code> | | <code>null</code> |
| [encryption_key_name](variables.tf#L50) | The fully-qualified resource name of the KMS key for cluster encryption. | <code>string</code> | | <code>null</code> |
| [initial_user](variables.tf#L56) | Alloy DB Cluster Initial User Credentials. | <code title="object&#40;&#123;&#10; user &#61; optional&#40;string&#41;,&#10; password &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [labels](variables.tf#L65) | User-defined labels for the alloydb cluster. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [location](variables.tf#L71) | Location where AlloyDb cluster will be deployed. | <code>string</code> | | <code>&#34;europe-west2&#34;</code> |
| [network_name](variables.tf#L77) | The network name of the project in which to provision resources. | <code>string</code> | | <code>&#34;multiple-readpool&#34;</code> |
| [read_pool_instance](variables.tf#L115) | List of Read Pool Instances to be created. | <code title="list&#40;object&#40;&#123;&#10; instance_id &#61; string&#10; display_name &#61; string&#10; node_count &#61; optional&#40;number, 1&#41;&#10; database_flags &#61; optional&#40;map&#40;string&#41;&#41;&#10; availability_type &#61; optional&#40;string&#41;&#10; gce_zone &#61; optional&#40;string&#41;&#10; machine_cpu_count &#61; optional&#40;number, 2&#41;&#10;&#125;&#41;&#41;">list&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#91;&#93;</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [cluster](outputs.tf#L17) | Cluster created. | ✓ |
| [cluster_id](outputs.tf#L23) | ID of the Alloy DB Cluster created. | |
| [primary_instance](outputs.tf#L28) | Primary instance created. | |
| [primary_instance_id](outputs.tf#L33) | ID of the primary instance created. | |
| [read_pool_instance_ids](outputs.tf#L38) | IDs of the read instances created. | |
<!-- END TFDOC -->

View File

@ -0,0 +1,160 @@
/**
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
locals {
quantity_based_retention_count = (
var.automated_backup_policy != null ? (var.automated_backup_policy.quantity_based_retention_count != null ? [var.automated_backup_policy.quantity_based_retention_count] : []) : []
)
read_pool_instance = (
var.read_pool_instance != null ?
{ for read_pool_instances in var.read_pool_instance : read_pool_instances.instance_id => read_pool_instances } : {}
)
time_based_retention_count = (
var.automated_backup_policy != null ? (var.automated_backup_policy.time_based_retention_count != null ? [var.automated_backup_policy.time_based_retention_count] : []) : []
)
}
resource "google_alloydb_cluster" "default" {
cluster_id = var.cluster_id
location = var.location
network = var.network_self_link
display_name = var.display_name
project = var.project_id
labels = var.labels
dynamic "automated_backup_policy" {
for_each = var.automated_backup_policy == null ? [] : [""]
content {
location = var.automated_backup_policy.location
backup_window = var.automated_backup_policy.backup_window
enabled = var.automated_backup_policy.enabled
labels = var.automated_backup_policy.labels
weekly_schedule {
days_of_week = automated_backup_policy.value.weekly_schedule.days_of_week
dynamic "start_times" {
for_each = { for i, time in automated_backup_policy.value.weekly_schedule.start_times : i => {
hours = tonumber(split(":", time)[0])
minutes = tonumber(split(":", time)[1])
seconds = tonumber(split(":", time)[2])
nanos = tonumber(split(":", time)[3])
}
}
content {
hours = start_times.value.hours
minutes = start_times.value.minutes
seconds = start_times.value.seconds
nanos = start_times.value.nanos
}
}
}
dynamic "quantity_based_retention" {
for_each = local.quantity_based_retention_count
content {
count = quantity_based_retention.value
}
}
dynamic "time_based_retention" {
for_each = local.time_based_retention_count
content {
retention_period = time_based_retention.value
}
}
dynamic "encryption_config" {
for_each = automated_backup_policy.value.backup_encryption_key_name == null ? [] : ["encryption_config"]
content {
kms_key_name = automated_backup_policy.value.backup_encryption_key_name
}
}
}
}
dynamic "initial_user" {
for_each = var.initial_user == null ? [] : ["initial_user"]
content {
user = var.initial_user.user
password = var.initial_user.password
}
}
dynamic "encryption_config" {
for_each = var.encryption_key_name == null ? [] : ["encryption_config"]
content {
kms_key_name = var.encryption_key_name
}
}
}
resource "google_alloydb_instance" "primary" {
cluster = google_alloydb_cluster.default.name
instance_id = var.primary_instance_config.instance_id
instance_type = "PRIMARY"
display_name = var.primary_instance_config.display_name
database_flags = var.primary_instance_config.database_flags
labels = var.primary_instance_config.labels
annotations = var.primary_instance_config.annotations
gce_zone = var.primary_instance_config.availability_type == "ZONAL" ? var.primary_instance_config.gce_zone : null
availability_type = var.primary_instance_config.availability_type
machine_config {
cpu_count = var.primary_instance_config.machine_cpu_count
}
}
resource "google_alloydb_instance" "read_pool" {
for_each = local.read_pool_instance
cluster = google_alloydb_cluster.default.name
instance_id = each.key
instance_type = "READ_POOL"
availability_type = each.value.availability_type
gce_zone = each.value.availability_type == "ZONAL" ? each.value.availability_type.gce_zone : null
read_pool_config {
node_count = each.value.node_count
}
database_flags = each.value.database_flags
machine_config {
cpu_count = each.value.machine_cpu_count
}
depends_on = [google_alloydb_instance.primary, google_compute_network.default, google_compute_global_address.private_ip_alloc, google_service_networking_connection.vpc_connection]
}
resource "google_compute_network" "default" {
name = var.network_name
}
resource "google_compute_global_address" "private_ip_alloc" {
name = "adb-all"
address_type = "INTERNAL"
purpose = "VPC_PEERING"
prefix_length = 16
network = google_compute_network.default.id
}
resource "google_service_networking_connection" "vpc_connection" {
network = google_compute_network.default.id
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [google_compute_global_address.private_ip_alloc.name]
}

View File

@ -0,0 +1,43 @@
/**
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
output "cluster" {
description = "Cluster created."
value = resource.google_alloydb_cluster.default.display_name
sensitive = true
}
output "cluster_id" {
description = "ID of the Alloy DB Cluster created."
value = google_alloydb_cluster.default.cluster_id
}
output "primary_instance" {
description = "Primary instance created."
value = resource.google_alloydb_instance.primary.display_name
}
output "primary_instance_id" {
description = "ID of the primary instance created."
value = google_alloydb_instance.primary.instance_id
}
output "read_pool_instance_ids" {
description = "IDs of the read instances created."
value = [
for rd, details in google_alloydb_instance.read_pool : details.instance_id
]
}

View File

@ -0,0 +1,127 @@
/**
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
variable "automated_backup_policy" {
description = "The automated backup policy for this cluster."
type = object({
location = optional(string)
backup_window = optional(string)
enabled = optional(bool)
weekly_schedule = optional(object({
days_of_week = optional(list(string))
start_times = list(string)
})),
quantity_based_retention_count = optional(number)
time_based_retention_count = optional(string)
labels = optional(map(string))
backup_encryption_key_name = optional(string)
})
default = null
}
variable "cluster_id" {
description = "The ID of the alloydb cluster."
type = string
validation {
condition = can(regex("^[a-z0-9-]+$", var.cluster_id))
error_message = "ERROR: Cluster ID must contain only Letters(lowercase), number, and hyphen."
}
}
variable "display_name" {
description = "Human readable display name for the Alloy DB Cluster."
type = string
default = null
}
variable "encryption_key_name" {
description = "The fully-qualified resource name of the KMS key for cluster encryption."
type = string
default = null
}
variable "initial_user" {
description = "Alloy DB Cluster Initial User Credentials."
type = object({
user = optional(string),
password = string
})
default = null
}
variable "labels" {
description = "User-defined labels for the alloydb cluster."
type = map(string)
default = {}
}
variable "location" {
description = "Location where AlloyDb cluster will be deployed."
type = string
default = "europe-west2"
}
variable "network_name" {
description = "The network name of the project in which to provision resources."
type = string
default = "multiple-readpool"
}
variable "network_self_link" {
description = "Network ID where the AlloyDb cluster will be deployed."
type = string
}
variable "primary_instance_config" {
description = "Primary cluster configuration that supports read and write operations."
type = object({
instance_id = string,
display_name = optional(string),
database_flags = optional(map(string))
labels = optional(map(string))
annotations = optional(map(string))
gce_zone = optional(string)
availability_type = optional(string)
machine_cpu_count = optional(number, 2),
})
validation {
condition = can(regex("^(2|4|8|16|32|64)$", var.primary_instance_config.machine_cpu_count))
error_message = "cpu count must be one of [2 4 8 16 32 64]."
}
validation {
condition = can(regex("^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$", var.primary_instance_config.instance_id))
error_message = "Primary Instance ID should satisfy the following pattern ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$."
}
}
variable "project_id" {
description = "The ID of the project in which to provision resources."
type = string
}
variable "read_pool_instance" {
description = "List of Read Pool Instances to be created."
type = list(object({
instance_id = string
display_name = string
node_count = optional(number, 1)
database_flags = optional(map(string))
availability_type = optional(string)
gce_zone = optional(string)
machine_cpu_count = optional(number, 2)
}))
default = []
}

View File

@ -0,0 +1,29 @@
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# 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.4.4"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 4.82.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
version = ">= 4.82.0" # tftest
}
}
}

View File

@ -0,0 +1,89 @@
# API Gateway
This module allows creating an API with its associated API config and API gateway. It also allows you grant IAM roles on the created resources.
# Examples
## Basic example
```hcl
module "gateway" {
source = "./fabric/modules/api-gateway"
project_id = "my-project"
api_id = "api"
region = "europe-west1"
spec = <<EOT
# The OpenAPI spec contents
# ...
EOT
}
# tftest modules=1 resources=4 inventory=basic.yaml
```
## Use existing service account
```hcl
module "gateway" {
source = "./fabric/modules/api-gateway"
project_id = "my-project"
api_id = "api"
region = "europe-west1"
service_account_email = "sa@my-project.iam.gserviceaccount.com"
iam = {
"roles/apigateway.admin" = ["user:user@example.com"]
}
spec = <<EOT
# The OpenAPI spec contents
# ...
EOT
}
# tftest modules=1 resources=7 inventory=existing-sa.yaml
```
## Create service account
```hcl
module "gateway" {
source = "./fabric/modules/api-gateway"
project_id = "my-project"
api_id = "api"
region = "europe-west1"
service_account_create = true
iam = {
"roles/apigateway.admin" = ["user:mirene@google.com"]
"roles/apigateway.viewer" = ["user:mirene@google.com"]
}
spec = <<EOT
# The OpenAPI spec contents
# ...
EOT
}
# tftest modules=1 resources=11 inventory=create-sa.yaml
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [api_id](variables.tf#L17) | API identifier. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L34) | Project identifier. | <code>string</code> | ✓ | |
| [region](variables.tf#L39) | Region. | <code>string</code> | ✓ | |
| [spec](variables.tf#L56) | String with the contents of the OpenAPI spec. | <code>string</code> | ✓ | |
| [iam](variables.tf#L22) | IAM bindings for the API in {ROLE => [MEMBERS]} format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>null</code> |
| [labels](variables.tf#L28) | Map of labels. | <code>map&#40;string&#41;</code> | | <code>null</code> |
| [service_account_create](variables.tf#L44) | Flag indicating whether a service account needs to be created. | <code>bool</code> | | <code>false</code> |
| [service_account_email](variables.tf#L50) | Service account for creating API configs. | <code>string</code> | | <code>null</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [api](outputs.tf#L17) | API. | |
| [api_config](outputs.tf#L28) | API configs. | |
| [api_config_id](outputs.tf#L39) | The identifiers of the API configs. | |
| [api_id](outputs.tf#L50) | API identifier. | |
| [default_hostname](outputs.tf#L61) | The default host names of the API gateway. | |
| [gateway](outputs.tf#L72) | API gateways. | |
| [gateway_id](outputs.tf#L83) | The identifiers of the API gateways. | |
| [service_account](outputs.tf#L94) | Service account resource. | |
| [service_account_email](outputs.tf#L99) | The service account for creating API configs. | |
| [service_account_iam_email](outputs.tf#L104) | The service account for creating API configs. | |
<!-- END TFDOC -->

View File

@ -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.
*/
locals {
service_account_email = (
var.service_account_create
? (
length(google_service_account.service_account) > 0
? google_service_account.service_account[0].email
: null
)
: var.service_account_email
)
}
resource "google_api_gateway_api" "api" {
provider = google-beta
project = var.project_id
api_id = var.api_id
display_name = var.api_id
labels = var.labels
}
resource "google_service_account" "service_account" {
count = var.service_account_create ? 1 : 0
project = var.project_id
account_id = "sa-api-cfg-${google_api_gateway_api.api.api_id}"
display_name = "Service account to create API configs for ${google_api_gateway_api.api.api_id} API"
}
resource "google_api_gateway_api_config" "api_config" {
provider = google-beta
project = google_api_gateway_api.api.project
api = google_api_gateway_api.api.api_id
api_config_id = "api-cfg-${google_api_gateway_api.api.api_id}-${md5(var.spec)}"
display_name = "api-cfg-${google_api_gateway_api.api.api_id}-${md5(var.spec)}"
openapi_documents {
document {
path = "spec.yaml"
contents = base64encode(var.spec)
}
}
dynamic "gateway_config" {
for_each = local.service_account_email == null ? [] : [""]
content {
backend_config {
google_service_account = local.service_account_email
}
}
}
lifecycle {
create_before_destroy = true
}
}
resource "google_api_gateway_gateway" "gateway" {
provider = google-beta
project = google_api_gateway_api_config.api_config.project
api_config = google_api_gateway_api_config.api_config.id
gateway_id = "gw-${google_api_gateway_api.api.api_id}"
display_name = "gw-${google_api_gateway_api.api.api_id}"
region = var.region
lifecycle {
create_before_destroy = true
}
}
resource "google_project_service" "service" {
project = google_api_gateway_gateway.gateway.project
service = google_api_gateway_api.api.managed_service
disable_on_destroy = true
disable_dependent_services = true
}
resource "google_api_gateway_api_iam_binding" "api_iam_bindings" {
for_each = coalesce(var.iam, {})
provider = google-beta
project = google_api_gateway_api.api.project
api = google_api_gateway_api.api.api_id
role = each.key
members = each.value
}
resource "google_api_gateway_api_config_iam_binding" "api_config_iam_bindings" {
for_each = coalesce(var.iam, {})
provider = google-beta
project = google_api_gateway_api_config.api_config.project
api = google_api_gateway_api.api.api_id
api_config = google_api_gateway_api_config.api_config.api_config_id
role = each.key
members = each.value
}
resource "google_api_gateway_gateway_iam_binding" "gateway_iam_bindings" {
for_each = coalesce(var.iam, {})
provider = google-beta
project = google_api_gateway_gateway.gateway.project
gateway = google_api_gateway_gateway.gateway.gateway_id
region = var.region
role = each.key
members = each.value
}

View File

@ -0,0 +1,107 @@
/**
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "api" {
description = "API."
value = google_api_gateway_api.api
depends_on = [
google_project_service.service,
google_api_gateway_api_iam_binding.api_iam_bindings,
google_api_gateway_api_config_iam_binding.api_config_iam_bindings,
google_api_gateway_gateway_iam_binding.gateway_iam_bindings
]
}
output "api_config" {
description = "API configs."
value = google_api_gateway_api_config.api_config
depends_on = [
google_project_service.service,
google_api_gateway_api_iam_binding.api_iam_bindings,
google_api_gateway_api_config_iam_binding.api_config_iam_bindings,
google_api_gateway_gateway_iam_binding.gateway_iam_bindings
]
}
output "api_config_id" {
description = "The identifiers of the API configs."
value = google_api_gateway_api_config.api_config.api_config_id
depends_on = [
google_project_service.service,
google_api_gateway_api_iam_binding.api_iam_bindings,
google_api_gateway_api_config_iam_binding.api_config_iam_bindings,
google_api_gateway_gateway_iam_binding.gateway_iam_bindings
]
}
output "api_id" {
description = "API identifier."
value = google_api_gateway_api.api.api_id
depends_on = [
google_project_service.service,
google_api_gateway_api_iam_binding.api_iam_bindings,
google_api_gateway_api_config_iam_binding.api_config_iam_bindings,
google_api_gateway_gateway_iam_binding.gateway_iam_bindings
]
}
output "default_hostname" {
description = "The default host names of the API gateway."
value = google_api_gateway_gateway.gateway.default_hostname
depends_on = [
google_project_service.service,
google_api_gateway_api_iam_binding.api_iam_bindings,
google_api_gateway_api_config_iam_binding.api_config_iam_bindings,
google_api_gateway_gateway_iam_binding.gateway_iam_bindings
]
}
output "gateway" {
description = "API gateways."
value = google_api_gateway_gateway.gateway
depends_on = [
google_project_service.service,
google_api_gateway_api_iam_binding.api_iam_bindings,
google_api_gateway_api_config_iam_binding.api_config_iam_bindings,
google_api_gateway_gateway_iam_binding.gateway_iam_bindings
]
}
output "gateway_id" {
description = "The identifiers of the API gateways."
value = google_api_gateway_gateway.gateway.gateway_id
depends_on = [
google_project_service.service,
google_api_gateway_api_iam_binding.api_iam_bindings,
google_api_gateway_api_config_iam_binding.api_config_iam_bindings,
google_api_gateway_gateway_iam_binding.gateway_iam_bindings
]
}
output "service_account" {
description = "Service account resource."
value = try(google_service_account.service_account[0], null)
}
output "service_account_email" {
description = "The service account for creating API configs."
value = local.service_account_email
}
output "service_account_iam_email" {
description = "The service account for creating API configs."
value = local.service_account_email == null ? null : "serviceAccount:${local.service_account_email}"
}

View File

@ -0,0 +1,59 @@
/**
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "api_id" {
description = "API identifier."
type = string
}
variable "iam" {
description = "IAM bindings for the API in {ROLE => [MEMBERS]} format."
type = map(list(string))
default = null
}
variable "labels" {
description = "Map of labels."
type = map(string)
default = null
}
variable "project_id" {
description = "Project identifier."
type = string
}
variable "region" {
description = "Region."
type = string
}
variable "service_account_create" {
description = "Flag indicating whether a service account needs to be created."
type = bool
default = false
}
variable "service_account_email" {
description = "Service account for creating API configs."
type = string
default = null
}
variable "spec" {
description = "String with the contents of the OpenAPI spec."
type = string
}

View File

@ -0,0 +1,29 @@
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# 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.4.4"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 4.82.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
version = ">= 4.82.0" # tftest
}
}
}

View File

@ -0,0 +1,340 @@
# Apigee
This module simplifies the creation of a Apigee resources (organization, environment groups, environment group attachments, environments, instances and instance attachments).
## Examples
<!-- BEGIN TOC -->
- [Examples](#examples)
- [Minimal example (CLOUD)](#minimal-example-cloud)
- [Minimal example with existing organization (CLOUD)](#minimal-example-with-existing-organization-cloud)
- [Disable VPC Peering (CLOUD)](#disable-vpc-peering-cloud)
- [All resources (CLOUD)](#all-resources-cloud)
- [All resources (HYBRID control plane)](#all-resources-hybrid-control-plane)
- [New environment group](#new-environment-group)
- [New environment](#new-environment)
- [New instance (VPC Peering Provisioning Mode)](#new-instance-vpc-peering-provisioning-mode)
- [New instance (Non VPC Peering Provisioning Mode)](#new-instance-non-vpc-peering-provisioning-mode)
- [New endpoint attachment](#new-endpoint-attachment)
- [Apigee add-ons](#apigee-add-ons)
- [Variables](#variables)
- [Outputs](#outputs)
<!-- END TOC -->
### Minimal example (CLOUD)
This example shows how to create to create an Apigee organization and deploy instance in it.
```hcl
module "apigee" {
source = "./fabric/modules/apigee"
project_id = var.project_id
organization = {
display_name = "Apigee"
billing_type = "PAYG"
analytics_region = "europe-west1"
authorized_network = var.vpc.id
runtime_type = "CLOUD"
}
envgroups = {
prod = ["prod.example.com"]
}
environments = {
apis-prod = {
display_name = "APIs prod"
description = "APIs Prod"
envgroups = ["prod"]
}
}
instances = {
europe-west1 = {
environments = ["apis-prod"]
runtime_ip_cidr_range = "10.32.0.0/22"
troubleshooting_ip_cidr_range = "10.64.0.0/28"
}
}
}
# tftest modules=1 resources=6 inventory=minimal-cloud.yaml
```
### Minimal example with existing organization (CLOUD)
This example shows how to create to work with an existing organization in the project. Note that in this case we don't specify the IP ranges for the instance, so it requests and allocates an available /22 and /28 CIDR block from Service Networking to deploy the instance.
```hcl
module "apigee" {
source = "./fabric/modules/apigee"
project_id = var.project_id
envgroups = {
prod = ["prod.example.com"]
}
environments = {
apis-prod = {
display_name = "APIs prod"
envgroups = ["prod"]
}
}
instances = {
europe-west1 = {
environments = ["apis-prod"]
}
}
}
# tftest modules=1 resources=5 inventory=minimal-cloud-no-org.yaml
```
### Disable VPC Peering (CLOUD)
When a new Apigee organization is created, it is automatically peered to the authorized network. You can prevent this from happening by using the `disable_vpc_peering` key in the `organization` variable, as shown below:
```hcl
module "apigee" {
source = "./fabric/modules/apigee"
project_id = var.project_id
organization = {
display_name = "Apigee"
billing_type = "PAYG"
analytics_region = "europe-west1"
runtime_type = "CLOUD"
disable_vpc_peering = true
}
envgroups = {
prod = ["prod.example.com"]
}
environments = {
apis-prod = {
display_name = "APIs prod"
envgroups = ["prod"]
}
}
instances = {
europe-west1 = {
environments = ["apis-prod"]
}
}
}
# tftest modules=1 resources=6 inventory=no-peering.yaml
```
### All resources (CLOUD)
```hcl
module "apigee" {
source = "./fabric/modules/apigee"
project_id = "my-project"
organization = {
display_name = "My Organization"
description = "My Organization"
authorized_network = "my-vpc"
runtime_type = "CLOUD"
billing_type = "PAYG"
database_encryption_key = "123456789"
analytics_region = "europe-west1"
}
envgroups = {
test = ["test.example.com"]
prod = ["prod.example.com"]
}
environments = {
apis-test = {
display_name = "APIs test"
description = "APIs Test"
envgroups = ["test"]
}
apis-prod = {
display_name = "APIs prod"
description = "APIs prod"
envgroups = ["prod"]
iam = {
"roles/viewer" = ["group:devops@myorg.com"]
}
}
}
instances = {
europe-west1 = {
runtime_ip_cidr_range = "10.0.4.0/22"
troubleshooting_ip_cidr_range = "10.1.1.0.0/28"
environments = ["apis-test"]
}
europe-west3 = {
runtime_ip_cidr_range = "10.0.8.0/22"
troubleshooting_ip_cidr_range = "10.1.16.0/28"
environments = ["apis-prod"]
enable_nat = true
}
}
endpoint_attachments = {
endpoint-backend-1 = {
region = "europe-west1"
service_attachment = "projects/my-project-1/serviceAttachments/gkebackend1"
}
endpoint-backend-2 = {
region = "europe-west1"
service_attachment = "projects/my-project-2/serviceAttachments/gkebackend2"
}
}
}
# tftest modules=1 resources=15
```
### All resources (HYBRID control plane)
```hcl
module "apigee" {
source = "./fabric/modules/apigee"
project_id = "my-project"
organization = {
display_name = "My Organization"
description = "My Organization"
runtime_type = "HYBRID"
analytics_region = "europe-west1"
}
envgroups = {
test = ["test.example.com"]
prod = ["prod.example.com"]
}
environments = {
apis-test = {
display_name = "APIs test"
description = "APIs Test"
envgroups = ["test"]
}
apis-prod = {
display_name = "APIs prod"
description = "APIs prod"
envgroups = ["prod"]
iam = {
"roles/viewer" = ["group:devops@myorg.com"]
}
}
}
}
# tftest modules=1 resources=8
```
### New environment group
```hcl
module "apigee" {
source = "./fabric/modules/apigee"
project_id = "my-project"
envgroups = {
test = ["test.example.com"]
}
}
# tftest modules=1 resources=1
```
### New environment
```hcl
module "apigee" {
source = "./fabric/modules/apigee"
project_id = "my-project"
environments = {
apis-test = {
display_name = "APIs test"
description = "APIs Test"
}
}
}
# tftest modules=1 resources=1
```
### New instance (VPC Peering Provisioning Mode)
```hcl
module "apigee" {
source = "./fabric/modules/apigee"
project_id = "my-project"
instances = {
europe-west1 = {
runtime_ip_cidr_range = "10.0.4.0/22"
troubleshooting_ip_cidr_range = "10.1.1.0/28"
}
}
}
# tftest modules=1 resources=1
```
### New instance (Non VPC Peering Provisioning Mode)
```hcl
module "apigee" {
source = "./fabric/modules/apigee"
project_id = "my-project"
organization = {
display_name = "My Organization"
description = "My Organization"
runtime_type = "CLOUD"
billing_type = "Pay-as-you-go"
database_encryption_key = "123456789"
analytics_region = "europe-west1"
disable_vpc_peering = true
}
instances = {
europe-west1 = {}
}
}
# tftest modules=1 resources=2
```
### New endpoint attachment
Endpoint attachments allow to implement [Apigee southbound network patterns](https://cloud.google.com/apigee/docs/api-platform/architecture/southbound-networking-patterns-endpoints#create-the-psc-attachments).
```hcl
module "apigee" {
source = "./fabric/modules/apigee"
project_id = "my-project"
endpoint_attachments = {
endpoint-backend-1 = {
region = "europe-west1"
service_attachment = "projects/my-project-1/serviceAttachments/gkebackend1"
}
}
}
# tftest modules=1 resources=1
```
### Apigee add-ons
```hcl
module "apigee" {
source = "./fabric/modules/apigee"
project_id = "my-project"
addons_config = {
monetization = true
}
}
# tftest modules=1 resources=1
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [project_id](variables.tf#L117) | Project ID. | <code>string</code> | ✓ | |
| [addons_config](variables.tf#L17) | Addons configuration. | <code title="object&#40;&#123;&#10; advanced_api_ops &#61; optional&#40;bool, false&#41;&#10; api_security &#61; optional&#40;bool, false&#41;&#10; connectors_platform &#61; optional&#40;bool, false&#41;&#10; integration &#61; optional&#40;bool, false&#41;&#10; monetization &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [endpoint_attachments](variables.tf#L29) | Endpoint attachments. | <code title="map&#40;object&#40;&#123;&#10; region &#61; string&#10; service_attachment &#61; string&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [envgroups](variables.tf#L39) | Environment groups (NAME => [HOSTNAMES]). | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [environments](variables.tf#L46) | Environments. | <code title="map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string&#41;&#10; description &#61; optional&#40;string, &#34;Terraform-managed&#34;&#41;&#10; deployment_type &#61; optional&#40;string&#41;&#10; api_proxy_type &#61; optional&#40;string&#41;&#10; node_config &#61; optional&#40;object&#40;&#123;&#10; min_node_count &#61; optional&#40;number&#41;&#10; max_node_count &#61; optional&#40;number&#41;&#10; &#125;&#41;&#41;&#10; iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#41;&#10; envgroups &#61; optional&#40;list&#40;string&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [instances](variables.tf#L64) | Instances ([REGION] => [INSTANCE]). | <code title="map&#40;object&#40;&#123;&#10; name &#61; optional&#40;string&#41;&#10; display_name &#61; optional&#40;string&#41;&#10; description &#61; optional&#40;string, &#34;Terraform-managed&#34;&#41;&#10; runtime_ip_cidr_range &#61; optional&#40;string&#41;&#10; troubleshooting_ip_cidr_range &#61; optional&#40;string&#41;&#10; disk_encryption_key &#61; optional&#40;string&#41;&#10; consumer_accept_list &#61; optional&#40;list&#40;string&#41;&#41;&#10; enable_nat &#61; optional&#40;bool, false&#41;&#10; environments &#61; optional&#40;list&#40;string&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [organization](variables.tf#L89) | Apigee organization. If set to null the organization must already exist. | <code title="object&#40;&#123;&#10; display_name &#61; optional&#40;string&#41;&#10; description &#61; optional&#40;string, &#34;Terraform-managed&#34;&#41;&#10; authorized_network &#61; optional&#40;string&#41;&#10; runtime_type &#61; optional&#40;string, &#34;CLOUD&#34;&#41;&#10; billing_type &#61; optional&#40;string&#41;&#10; database_encryption_key &#61; optional&#40;string&#41;&#10; analytics_region &#61; optional&#40;string, &#34;europe-west1&#34;&#41;&#10; retention &#61; optional&#40;string&#41;&#10; disable_vpc_peering &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [endpoint_attachment_hosts](outputs.tf#L17) | Endpoint hosts. | |
| [envgroups](outputs.tf#L22) | Environment groups. | |
| [environments](outputs.tf#L27) | Environment. | |
| [instances](outputs.tf#L32) | Instances. | |
| [nat_ips](outputs.tf#L37) | NAT IP addresses used in instances. | |
| [org_id](outputs.tf#L45) | Organization ID. | |
| [org_name](outputs.tf#L50) | Organization name. | |
| [organization](outputs.tf#L55) | Organization. | |
| [service_attachments](outputs.tf#L60) | Service attachments. | |
<!-- END TFDOC -->

View File

@ -0,0 +1,155 @@
/**
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
locals {
org_id = try(google_apigee_organization.organization[0].id, "organizations/${var.project_id}")
org_name = try(google_apigee_organization.organization[0].name, var.project_id)
}
resource "google_apigee_organization" "organization" {
count = var.organization == null ? 0 : 1
analytics_region = var.organization.analytics_region
project_id = var.project_id
authorized_network = var.organization.authorized_network
billing_type = var.organization.billing_type
runtime_type = var.organization.runtime_type
runtime_database_encryption_key_name = var.organization.database_encryption_key
retention = var.organization.retention
disable_vpc_peering = var.organization.disable_vpc_peering
}
resource "google_apigee_envgroup" "envgroups" {
for_each = var.envgroups
name = each.key
hostnames = each.value
org_id = local.org_id
}
resource "google_apigee_environment" "environments" {
for_each = var.environments
name = each.key
display_name = each.value.display_name
description = each.value.description
deployment_type = each.value.deployment_type
api_proxy_type = each.value.api_proxy_type
dynamic "node_config" {
for_each = try(each.value.node_config, null) != null ? [""] : []
content {
min_node_count = each.value.node_config.min_node_count
max_node_count = each.value.node_config.max_node_count
}
}
org_id = local.org_id
lifecycle {
ignore_changes = [
node_config["current_aggregate_node_count"]
]
}
}
resource "google_apigee_envgroup_attachment" "envgroup_attachments" {
for_each = merge(concat([for k1, v1 in var.environments : {
for v2 in coalesce(v1.envgroups, []) : "${k1}-${v2}" => {
environment = k1
envgroup = v2
}
}])...)
envgroup_id = try(google_apigee_envgroup.envgroups[each.value.envgroup].id, each.value.envgroup)
environment = google_apigee_environment.environments[each.value.environment].name
}
resource "google_apigee_environment_iam_binding" "binding" {
for_each = merge(concat([for k1, v1 in var.environments : {
for k2, v2 in coalesce(v1.iam, {}) : "${k1}-${k2}" => {
environment = "${k1}"
role = k2
members = v2
}
}])...)
org_id = local.org_id
env_id = google_apigee_environment.environments[each.value.environment].name
role = each.value.role
members = each.value.members
}
resource "google_apigee_instance" "instances" {
for_each = var.instances
name = coalesce(each.value.name, "instance-${each.key}")
display_name = each.value.display_name
description = each.value.description
location = each.key
org_id = local.org_id
ip_range = (
compact([each.value.runtime_ip_cidr_range, each.value.troubleshooting_ip_cidr_range]) == []
? null
: join(",", compact([each.value.runtime_ip_cidr_range, each.value.troubleshooting_ip_cidr_range]))
)
disk_encryption_key_name = each.value.disk_encryption_key
consumer_accept_list = each.value.consumer_accept_list
}
resource "google_apigee_nat_address" "apigee_nat" {
for_each = {
for k, v in var.instances :
k => google_apigee_instance.instances[k].id
if v.enable_nat
}
name = each.key
instance_id = each.value
}
resource "google_apigee_instance_attachment" "instance_attachments" {
for_each = merge(concat([for k1, v1 in var.instances : {
for v2 in coalesce(v1.environments, []) :
"${k1}-${v2}" => {
instance = k1
environment = v2
}
}])...)
instance_id = google_apigee_instance.instances[each.value.instance].id
environment = try(google_apigee_environment.environments[each.value.environment].name,
"${local.org_id}/environments/${each.value.environment}")
}
resource "google_apigee_endpoint_attachment" "endpoint_attachments" {
for_each = var.endpoint_attachments
org_id = local.org_id
endpoint_attachment_id = each.key
location = each.value.region
service_attachment = each.value.service_attachment
}
resource "google_apigee_addons_config" "addons_config" {
for_each = toset(var.addons_config == null ? [] : [""])
org = local.org_name
addons_config {
advanced_api_ops_config {
enabled = var.addons_config.advanced_api_ops
}
api_security_config {
enabled = var.addons_config.api_security
}
connectors_platform_config {
enabled = var.addons_config.connectors_platform
}
integration_config {
enabled = var.addons_config.integration
}
monetization_config {
enabled = var.addons_config.monetization
}
}
}

View File

@ -0,0 +1,63 @@
/**
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
output "endpoint_attachment_hosts" {
description = "Endpoint hosts."
value = { for k, v in google_apigee_endpoint_attachment.endpoint_attachments : k => v.host }
}
output "envgroups" {
description = "Environment groups."
value = try(google_apigee_envgroup.envgroups, null)
}
output "environments" {
description = "Environment."
value = try(google_apigee_environment.environments, null)
}
output "instances" {
description = "Instances."
value = try(google_apigee_instance.instances, null)
}
output "nat_ips" {
description = "NAT IP addresses used in instances."
value = {
for k, v in google_apigee_nat_address.apigee_nat :
k => v.ip_address
}
}
output "org_id" {
description = "Organization ID."
value = local.org_id
}
output "org_name" {
description = "Organization name."
value = try(google_apigee_organization.organization[0].name, var.project_id)
}
output "organization" {
description = "Organization."
value = try(google_apigee_organization.organization[0], null)
}
output "service_attachments" {
description = "Service attachments."
value = { for k, v in google_apigee_instance.instances : k => v.service_attachment }
}

View File

@ -0,0 +1,120 @@
/**
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
variable "addons_config" {
description = "Addons configuration."
type = object({
advanced_api_ops = optional(bool, false)
api_security = optional(bool, false)
connectors_platform = optional(bool, false)
integration = optional(bool, false)
monetization = optional(bool, false)
})
default = null
}
variable "endpoint_attachments" {
description = "Endpoint attachments."
type = map(object({
region = string
service_attachment = string
}))
default = {}
nullable = false
}
variable "envgroups" {
description = "Environment groups (NAME => [HOSTNAMES])."
type = map(list(string))
default = {}
nullable = false
}
variable "environments" {
description = "Environments."
type = map(object({
display_name = optional(string)
description = optional(string, "Terraform-managed")
deployment_type = optional(string)
api_proxy_type = optional(string)
node_config = optional(object({
min_node_count = optional(number)
max_node_count = optional(number)
}))
iam = optional(map(list(string)))
envgroups = optional(list(string))
}))
default = {}
nullable = false
}
variable "instances" {
description = "Instances ([REGION] => [INSTANCE])."
type = map(object({
name = optional(string)
display_name = optional(string)
description = optional(string, "Terraform-managed")
runtime_ip_cidr_range = optional(string)
troubleshooting_ip_cidr_range = optional(string)
disk_encryption_key = optional(string)
consumer_accept_list = optional(list(string))
enable_nat = optional(bool, false)
environments = optional(list(string))
}))
validation {
condition = alltrue([
for k, v in var.instances :
# has troubleshooting_ip => has runtime_ip
v.runtime_ip_cidr_range != null || v.troubleshooting_ip_cidr_range == null
])
error_message = "Using a troubleshooting range requires specifying a runtime range too."
}
default = {}
nullable = false
}
variable "organization" {
description = "Apigee organization. If set to null the organization must already exist."
type = object({
display_name = optional(string)
description = optional(string, "Terraform-managed")
authorized_network = optional(string)
runtime_type = optional(string, "CLOUD")
billing_type = optional(string)
database_encryption_key = optional(string)
analytics_region = optional(string, "europe-west1")
retention = optional(string)
disable_vpc_peering = optional(bool, false)
})
validation {
condition = var.organization == null || (
try(var.organization.runtime_type, null) == "CLOUD" || !try(var.organization.disable_vpc_peering, false)
)
error_message = "Disabling the VPC peering can only be done in organization using the CLOUD runtime."
}
validation {
condition = var.organization == null || (
try(var.organization.authorized_network, null) == null || !try(var.organization.disable_vpc_peering, false)
)
error_message = "Disabling the VPC peering is mutually exclusive with authorized_network."
}
default = null
}
variable "project_id" {
description = "Project ID."
type = string
}

View File

@ -0,0 +1,29 @@
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# 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.4.4"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 4.82.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
version = ">= 4.82.0" # tftest
}
}
}

View File

@ -0,0 +1,125 @@
# Google Cloud Artifact Registry Module
This module simplifies the creation of repositories using Google Cloud Artifact Registry.
<!-- BEGIN TOC -->
- [Standard Repository](#standard-repository)
- [Remote and Virtual Repositories](#remote-and-virtual-repositories)
- [Additional Docker and Maven Options](#additional-docker-and-maven-options)
- [Variables](#variables)
- [Outputs](#outputs)
<!-- END TOC -->
## Standard Repository
```hcl
module "docker_artifact_registry" {
source = "./fabric/modules/artifact-registry"
project_id = "myproject"
location = "europe-west1"
name = "myregistry"
iam = {
"roles/artifactregistry.admin" = ["group:cicd@example.com"]
}
}
# tftest modules=1 resources=2
```
## Remote and Virtual Repositories
```hcl
module "registry-local" {
source = "./fabric/modules/artifact-registry"
project_id = var.project_id
location = "europe-west1"
name = "local"
format = { python = {} }
}
module "registry-remote" {
source = "./fabric/modules/artifact-registry"
project_id = var.project_id
location = "europe-west1"
name = "remote"
format = { python = {} }
mode = { remote = true }
}
module "registry-virtual" {
source = "./fabric/modules/artifact-registry"
project_id = var.project_id
location = "europe-west1"
name = "virtual"
format = { python = {} }
mode = {
virtual = {
remote = {
repository = module.registry-remote.id
priority = 1
}
local = {
repository = module.registry-local.id
priority = 10
}
}
}
}
# tftest modules=3 resources=3 inventory=remote-virtual.yaml
```
## Additional Docker and Maven Options
```hcl
module "registry-docker" {
source = "./fabric/modules/artifact-registry"
project_id = var.project_id
location = "europe-west1"
name = "docker"
format = {
docker = {
immutable_tags = true
}
}
}
module "registry-maven" {
source = "./fabric/modules/artifact-registry"
project_id = var.project_id
location = "europe-west1"
name = "maven"
format = {
maven = {
allow_snapshot_overwrites = true
version_policy = "RELEASE"
}
}
}
# tftest modules=2 resources=2
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [location](variables.tf#L68) | Registry location. Use `gcloud beta artifacts locations list' to get valid values. | <code>string</code> | ✓ | |
| [name](variables.tf#L93) | Registry name. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L98) | Registry project id. | <code>string</code> | ✓ | |
| [description](variables.tf#L17) | An optional description for the repository. | <code>string</code> | | <code>&#34;Terraform-managed registry&#34;</code> |
| [encryption_key](variables.tf#L23) | The KMS key name to use for encryption at rest. | <code>string</code> | | <code>null</code> |
| [format](variables.tf#L29) | Repository format. | <code title="object&#40;&#123;&#10; apt &#61; optional&#40;object&#40;&#123;&#125;&#41;&#41;&#10; docker &#61; optional&#40;object&#40;&#123;&#10; immutable_tags &#61; optional&#40;bool&#41;&#10; &#125;&#41;&#41;&#10; kfp &#61; optional&#40;object&#40;&#123;&#125;&#41;&#41;&#10; go &#61; optional&#40;object&#40;&#123;&#125;&#41;&#41;&#10; maven &#61; optional&#40;object&#40;&#123;&#10; allow_snapshot_overwrites &#61; optional&#40;bool&#41;&#10; version_policy &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; npm &#61; optional&#40;object&#40;&#123;&#125;&#41;&#41;&#10; python &#61; optional&#40;object&#40;&#123;&#125;&#41;&#41;&#10; yum &#61; optional&#40;object&#40;&#123;&#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123; docker &#61; &#123;&#125; &#125;</code> |
| [iam](variables.tf#L56) | IAM bindings in {ROLE => [MEMBERS]} format. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [labels](variables.tf#L62) | Labels to be attached to the registry. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [mode](variables.tf#L73) | Repository mode. | <code title="object&#40;&#123;&#10; standard &#61; optional&#40;bool&#41;&#10; remote &#61; optional&#40;bool&#41;&#10; virtual &#61; optional&#40;map&#40;object&#40;&#123;&#10; repository &#61; string&#10; priority &#61; number&#10; &#125;&#41;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123; standard &#61; true &#125;</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [id](outputs.tf#L17) | Fully qualified repository id. | |
| [image_path](outputs.tf#L22) | Repository path for images. | |
| [name](outputs.tf#L32) | Repository name. | |
<!-- END TFDOC -->

View File

@ -0,0 +1,115 @@
/**
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
locals {
format_string = one([for k, v in var.format : k if v != null])
mode_string = one([for k, v in var.mode : k if v != null && v != false])
}
resource "google_artifact_registry_repository" "registry" {
project = var.project_id
location = var.location
description = var.description
format = upper(local.format_string)
labels = var.labels
repository_id = var.name
mode = "${upper(local.mode_string)}_REPOSITORY"
kms_key_name = var.encryption_key
dynamic "docker_config" {
# TODO: open a bug on the provider for this permadiff
for_each = (
local.format_string == "docker" && try(var.format.docker.immutable_tags, null) == true
? [""]
: []
)
content {
immutable_tags = var.format.docker.immutable_tags
}
}
dynamic "maven_config" {
for_each = local.format_string == "maven" ? [""] : []
content {
allow_snapshot_overwrites = var.format.maven.allow_snapshot_overwrites
version_policy = var.format.maven.version_policy
}
}
dynamic "remote_repository_config" {
for_each = local.mode_string == "remote" ? [""] : []
content {
dynamic "docker_repository" {
for_each = local.format_string == "docker" ? [""] : []
content {
public_repository = "DOCKER_HUB"
}
}
dynamic "maven_repository" {
for_each = local.format_string == "maven" ? [""] : []
content {
public_repository = "MAVEN_CENTRAL"
}
}
dynamic "npm_repository" {
for_each = local.format_string == "npm" ? [""] : []
content {
public_repository = "NPMJS"
}
}
dynamic "python_repository" {
for_each = local.format_string == "python" ? [""] : []
content {
public_repository = "PYPI"
}
}
}
}
dynamic "virtual_repository_config" {
for_each = local.mode_string == "virtual" ? [""] : []
content {
dynamic "upstream_policies" {
for_each = var.mode.virtual
content {
id = upstream_policies.key
repository = upstream_policies.value.repository
priority = upstream_policies.value.priority
}
}
}
}
lifecycle {
precondition {
condition = local.mode_string != "remote" || contains(
["docker", "maven", "npm", "python"], local.format_string
)
error_message = "Invalid format for remote repository."
}
}
}
resource "google_artifact_registry_repository_iam_binding" "bindings" {
provider = google-beta
for_each = var.iam
project = var.project_id
location = google_artifact_registry_repository.registry.location
repository = google_artifact_registry_repository.registry.name
role = each.key
members = each.value
}

View File

@ -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.
*/
output "id" {
description = "Fully qualified repository id."
value = google_artifact_registry_repository.registry.id
}
output "image_path" {
description = "Repository path for images."
value = join("/", [
"${var.location}-docker.pkg.dev",
var.project_id,
var.name
])
depends_on = [google_artifact_registry_repository.registry]
}
output "name" {
description = "Repository name."
value = google_artifact_registry_repository.registry.name
}

View File

@ -0,0 +1,101 @@
/**
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
variable "description" {
description = "An optional description for the repository."
type = string
default = "Terraform-managed registry"
}
variable "encryption_key" {
description = "The KMS key name to use for encryption at rest."
type = string
default = null
}
variable "format" {
description = "Repository format."
type = object({
apt = optional(object({}))
docker = optional(object({
immutable_tags = optional(bool)
}))
kfp = optional(object({}))
go = optional(object({}))
maven = optional(object({
allow_snapshot_overwrites = optional(bool)
version_policy = optional(string)
}))
npm = optional(object({}))
python = optional(object({}))
yum = optional(object({}))
})
nullable = false
default = { docker = {} }
validation {
condition = (
length([for k, v in var.format : k if v != null]) == 1
)
error_message = "Multiple or zero formats are not supported."
}
}
variable "iam" {
description = "IAM bindings in {ROLE => [MEMBERS]} format."
type = map(list(string))
default = {}
}
variable "labels" {
description = "Labels to be attached to the registry."
type = map(string)
default = {}
}
variable "location" {
description = "Registry location. Use `gcloud beta artifacts locations list' to get valid values."
type = string
}
variable "mode" {
description = "Repository mode."
type = object({
standard = optional(bool)
remote = optional(bool)
virtual = optional(map(object({
repository = string
priority = number
})))
})
nullable = false
default = { standard = true }
validation {
condition = (
length([for k, v in var.mode : k if v != null && v != false]) == 1
)
error_message = "Multiple or zero modes are not supported."
}
}
variable "name" {
description = "Registry name."
type = string
}
variable "project_id" {
description = "Registry project id."
type = string
}

View File

@ -0,0 +1,29 @@
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# 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.4.4"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 4.82.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
version = ">= 4.82.0" # tftest
}
}
}

View File

@ -0,0 +1,312 @@
# Google Cloud Bigquery Module
This module allows managing a single BigQuery dataset, including access configuration, tables and views.
## TODO
- [ ] check for dynamic values in tables and views
- [ ] add support for external tables
## Examples
### Simple dataset with access configuration
Access configuration defaults to using the separate `google_bigquery_dataset_access` resource, so as to leave the default dataset access rules untouched.
You can choose to manage the `google_bigquery_dataset` access rules instead via the `dataset_access` variable, but be sure to always have at least one `OWNER` access and to avoid duplicating accesses, or `terraform apply` will fail.
The access variables are split into `access` and `access_identities` variables, so that dynamic values can be passed in for identities (eg a service account email generated by a different module or resource).
```hcl
module "bigquery-dataset" {
source = "./fabric/modules/bigquery-dataset"
project_id = "my-project"
id = "my-dataset"
access = {
reader-group = { role = "READER", type = "group" }
owner = { role = "OWNER", type = "user" }
project_owners = { role = "OWNER", type = "special_group" }
view_1 = { role = "READER", type = "view" }
}
access_identities = {
reader-group = "playground-test@ludomagno.net"
owner = "ludo@ludomagno.net"
project_owners = "projectOwners"
view_1 = "my-project|my-dataset|my-table"
}
}
# tftest modules=1 resources=5 inventory=simple.yaml
```
### IAM roles
Access configuration can also be specified via IAM instead of basic roles via the `iam` variable. When using IAM, basic roles cannot be used via the `access` family variables.
```hcl
module "bigquery-dataset" {
source = "./fabric/modules/bigquery-dataset"
project_id = "my-project"
id = "my-dataset"
iam = {
"roles/bigquery.dataOwner" = ["user:user1@example.org"]
}
}
# tftest modules=1 resources=2 inventory=iam.yaml
```
### Authorized Views, Datasets, and Routines
You can specify authorized [views](https://cloud.google.com/bigquery/docs/authorized-views), [datasets](https://cloud.google.com/bigquery/docs/authorized-datasets?hl=en), and [routines](https://cloud.google.com/bigquery/docs/authorized-routines) via the `authorized_views`, `authorized_datasets` and `authorized_routines` variables, respectively.
```hcl
// Create private BigQuery dataset that will not be publicly accessible, except via the authorized BigQuery resources
module "bigquery-dataset-private" {
source = "./fabric/modules/bigquery-dataset"
project_id = "private_project"
id = "private_dataset"
authorized_views = [
{
project_id = "auth_view_project"
dataset_id = "auth_view_dataset"
table_id = "auth_view"
}
]
authorized_datasets = [
{
project_id = "auth_dataset_project"
dataset_id = "auth_dataset"
}
]
authorized_routines = [
{
project_id = "auth_routine_project"
dataset_id = "auth_routine_dataset"
routine_id = "auth_routine"
}
]
}
// Create authorized view in a public dataset
module "bigquery-authorized-views-dataset-public" {
source = "./fabric/modules/bigquery-dataset"
project_id = "auth_view_project"
id = "auth_view_dataset"
views = {
auth_view = {
friendly_name = "Public"
labels = {}
query = "SELECT * FROM `private_project.private_dataset.private_table`"
use_legacy_sql = false
deletion_protection = true
}
}
}
// Create public authorized dataset
module "bigquery-authorized-dataset-public" {
source = "./fabric/modules/bigquery-dataset"
project_id = "auth_dataset_project"
id = "auth_dataset"
}
// Create public authorized routine
module "bigquery-authorized-authorized-routine-dataset-public" {
source = "./fabric/modules/bigquery-dataset"
project_id = "auth_routine_project"
id = "auth_routine_dataset"
}
resource "google_bigquery_routine" "public-routine" {
dataset_id = module.bigquery-authorized-authorized-routine-dataset-public.dataset_id
routine_id = "auth_routine"
routine_type = "TABLE_VALUED_FUNCTION"
language = "SQL"
definition_body = <<-EOS
SELECT 1 + value AS value
EOS
arguments {
name = "value"
argument_kind = "FIXED_TYPE"
data_type = jsonencode({ "typeKind" = "INT64" })
}
return_table_type = jsonencode({ "columns" = [
{ "name" = "value", "type" = { "typeKind" = "INT64" } },
] })
}
# tftest modules=4 resources=9 inventory=authorized_resources.yaml
```
Authorized views can be specified both using the standard `access` options and the `authorized_views` blocks. The example configuration below uses both blocks, and will create a dataset with three authorized views `view_id_1`, `view_id_2`, and `view_id_3`.
```hcl
module "bigquery-dataset" {
source = "./fabric/modules/bigquery-dataset"
project_id = "my-project"
id = "my-dataset"
authorized_views = [
{
project_id = "view_project"
dataset_id = "view_dataset"
table_id = "view_id_1"
},
{
project_id = "view_project"
dataset_id = "view_dataset"
table_id = "view_id_2"
}
]
access = {
view_2 = { role = "READER", type = "view" }
view_3 = { role = "READER", type = "view" }
}
access_identities = {
view_2 = "view_project|view_dataset|view_id_2"
view_3 = "view_project|view_dataset|view_id_3"
}
}
# tftest modules=1 resources=4 inventory=authorized_resources_views.yaml
```
### Dataset options
Dataset options are set via the `options` variable. all options must be specified, but a `null` value can be set to options that need to use defaults.
```hcl
module "bigquery-dataset" {
source = "./fabric/modules/bigquery-dataset"
project_id = "my-project"
id = "my-dataset"
options = {
default_table_expiration_ms = 3600000
default_partition_expiration_ms = null
delete_contents_on_destroy = false
max_time_travel_hours = 168
}
}
# tftest modules=1 resources=1 inventory=options.yaml
```
### Tables and views
Tables are created via the `tables` variable, or the `view` variable for views. Support for external tables will be added in a future release.
```hcl
locals {
countries_schema = jsonencode([
{ name = "country", type = "STRING" },
{ name = "population", type = "INT64" },
])
}
module "bigquery-dataset" {
source = "./fabric/modules/bigquery-dataset"
project_id = "my-project"
id = "my_dataset"
tables = {
countries = {
friendly_name = "Countries"
schema = local.countries_schema
deletion_protection = true
}
}
}
# tftest modules=1 resources=2 inventory=tables.yaml
```
If partitioning is needed, populate the `partitioning` variable using either the `time` or `range` attribute.
```hcl
locals {
countries_schema = jsonencode([
{ name = "country", type = "STRING" },
{ name = "population", type = "INT64" },
])
}
module "bigquery-dataset" {
source = "./fabric/modules/bigquery-dataset"
project_id = "my-project"
id = "my-dataset"
tables = {
table_a = {
deletion_protection = true
friendly_name = "Table a"
schema = local.countries_schema
partitioning = {
time = { type = "DAY", expiration_ms = null }
}
}
}
}
# tftest modules=1 resources=2 inventory=partitioning.yaml
```
To create views use the `view` variable. If you're querying a table created by the same module `terraform apply` will initially fail and eventually succeed once the underlying table has been created. You can probably also use the module's output in the view's query to create a dependency on the table.
```hcl
locals {
countries_schema = jsonencode([
{ name = "country", type = "STRING" },
{ name = "population", type = "INT64" },
])
}
module "bigquery-dataset" {
source = "./fabric/modules/bigquery-dataset"
project_id = "my-project"
id = "my_dataset"
tables = {
countries = {
friendly_name = "Countries"
schema = local.countries_schema
deletion_protection = true
}
}
views = {
population = {
friendly_name = "Population"
query = "SELECT SUM(population) FROM my_dataset.countries"
use_legacy_sql = false
deletion_protection = true
}
}
}
# tftest modules=1 resources=3 inventory=views.yaml
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [id](variables.tf#L98) | Dataset id. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L128) | Id of the project where datasets will be created. | <code>string</code> | ✓ | |
| [access](variables.tf#L17) | Map of access rules with role and identity type. Keys are arbitrary and must match those in the `access_identities` variable, types are `domain`, `group`, `special_group`, `user`, `view`. | <code title="map&#40;object&#40;&#123;&#10; role &#61; string&#10; type &#61; string&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [access_identities](variables.tf#L33) | Map of access identities used for basic access roles. View identities have the format 'project_id\|dataset_id\|table_id'. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [authorized_datasets](variables.tf#L39) | An array of datasets to be authorized on the dataset. | <code title="list&#40;object&#40;&#123;&#10; dataset_id &#61; string,&#10; project_id &#61; string,&#10;&#125;&#41;&#41;">list&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#91;&#93;</code> |
| [authorized_routines](variables.tf#L48) | An array of authorized routine to be authorized on the dataset. | <code title="list&#40;object&#40;&#123;&#10; project_id &#61; string,&#10; dataset_id &#61; string,&#10; routine_id &#61; string&#10;&#125;&#41;&#41;">list&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#91;&#93;</code> |
| [authorized_views](variables.tf#L58) | An array of views to be authorized on the dataset. | <code title="list&#40;object&#40;&#123;&#10; dataset_id &#61; string,&#10; project_id &#61; string,&#10; table_id &#61; string &#35; this is the view id, but we keep table_id to stay consistent as the resource&#10;&#125;&#41;&#41;">list&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#91;&#93;</code> |
| [dataset_access](variables.tf#L68) | Set access in the dataset resource instead of using separate resources. | <code>bool</code> | | <code>false</code> |
| [description](variables.tf#L74) | Optional description. | <code>string</code> | | <code>&#34;Terraform managed.&#34;</code> |
| [encryption_key](variables.tf#L80) | Self link of the KMS key that will be used to protect destination table. | <code>string</code> | | <code>null</code> |
| [friendly_name](variables.tf#L86) | Dataset friendly name. | <code>string</code> | | <code>null</code> |
| [iam](variables.tf#L92) | IAM bindings in {ROLE => [MEMBERS]} format. Mutually exclusive with the access_* variables used for basic roles. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [labels](variables.tf#L103) | Dataset labels. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [location](variables.tf#L109) | Dataset location. | <code>string</code> | | <code>&#34;EU&#34;</code> |
| [options](variables.tf#L115) | Dataset options. | <code title="object&#40;&#123;&#10; default_collation &#61; optional&#40;string&#41;&#10; default_table_expiration_ms &#61; optional&#40;number&#41;&#10; default_partition_expiration_ms &#61; optional&#40;number&#41;&#10; delete_contents_on_destroy &#61; optional&#40;bool, false&#41;&#10; is_case_insensitive &#61; optional&#40;bool&#41;&#10; max_time_travel_hours &#61; optional&#40;number, 168&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [tables](variables.tf#L133) | Table definitions. Options and partitioning default to null. Partitioning can only use `range` or `time`, set the unused one to null. | <code title="map&#40;object&#40;&#123;&#10; deletion_protection &#61; optional&#40;bool&#41;&#10; description &#61; optional&#40;string, &#34;Terraform managed.&#34;&#41;&#10; friendly_name &#61; optional&#40;string&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; schema &#61; optional&#40;string&#41;&#10; options &#61; optional&#40;object&#40;&#123;&#10; clustering &#61; optional&#40;list&#40;string&#41;&#41;&#10; encryption_key &#61; optional&#40;string&#41;&#10; expiration_time &#61; optional&#40;number&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; partitioning &#61; optional&#40;object&#40;&#123;&#10; field &#61; optional&#40;string&#41;&#10; range &#61; optional&#40;object&#40;&#123;&#10; end &#61; number&#10; interval &#61; number&#10; start &#61; number&#10; &#125;&#41;&#41;&#10; time &#61; optional&#40;object&#40;&#123;&#10; expiration_ms &#61; number&#10; type &#61; string&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [views](variables.tf#L162) | View definitions. | <code title="map&#40;object&#40;&#123;&#10; query &#61; string&#10; deletion_protection &#61; optional&#40;bool&#41;&#10; description &#61; optional&#40;string, &#34;Terraform managed.&#34;&#41;&#10; friendly_name &#61; optional&#40;string&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; use_legacy_sql &#61; optional&#40;bool&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [dataset](outputs.tf#L17) | Dataset resource. | |
| [dataset_id](outputs.tf#L22) | Dataset id. | |
| [id](outputs.tf#L36) | Fully qualified dataset id. | |
| [self_link](outputs.tf#L50) | Dataset self link. | |
| [table_ids](outputs.tf#L64) | Map of fully qualified table ids keyed by table ids. | |
| [tables](outputs.tf#L69) | Table resources. | |
| [view_ids](outputs.tf#L74) | Map of fully qualified view ids keyed by view ids. | |
| [views](outputs.tf#L79) | View resources. | |
<!-- END TFDOC -->

View File

@ -0,0 +1,268 @@
/**
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 {
access_domain = { for k, v in var.access : k => v if v.type == "domain" }
access_group = { for k, v in var.access : k => v if v.type == "group" }
access_special = { for k, v in var.access : k => v if v.type == "special_group" }
access_user = { for k, v in var.access : k => v if v.type == "user" }
access_view = { for k, v in var.access : k => v if v.type == "view" }
identities_view = {
for k, v in local.access_view : k => try(
zipmap(
["project_id", "dataset_id", "table_id"],
split("|", var.access_identities[k])
),
{ project_id = null, dataset_id = null, table_id = null }
)
}
authorized_views = merge(
{ for access_key, view in local.identities_view : "${view["project_id"]}_${view["dataset_id"]}_${view["table_id"]}" => view },
{ for view in var.authorized_views : "${view["project_id"]}_${view["dataset_id"]}_${view["table_id"]}" => view })
authorized_datasets = { for dataset in var.authorized_datasets : "${dataset["project_id"]}_${dataset["dataset_id"]}" => dataset }
authorized_routines = { for routine in var.authorized_routines : "${routine["project_id"]}_${routine["dataset_id"]}_${routine["routine_id"]}" => routine }
}
resource "google_bigquery_dataset" "default" {
project = var.project_id
dataset_id = var.id
friendly_name = var.friendly_name
description = var.description
labels = var.labels
location = var.location
delete_contents_on_destroy = var.options.delete_contents_on_destroy
default_collation = var.options.default_collation
default_table_expiration_ms = var.options.default_table_expiration_ms
default_partition_expiration_ms = var.options.default_partition_expiration_ms
is_case_insensitive = var.options.is_case_insensitive
max_time_travel_hours = var.options.max_time_travel_hours
dynamic "access" {
for_each = var.dataset_access ? local.access_domain : {}
content {
role = access.value.role
domain = try(var.access_identities[access.key])
}
}
dynamic "access" {
for_each = var.dataset_access ? local.access_group : {}
content {
role = access.value.role
group_by_email = try(var.access_identities[access.key])
}
}
dynamic "access" {
for_each = var.dataset_access ? local.access_special : {}
content {
role = access.value.role
special_group = try(var.access_identities[access.key])
}
}
dynamic "access" {
for_each = var.dataset_access ? local.access_user : {}
content {
role = access.value.role
user_by_email = try(var.access_identities[access.key])
}
}
dynamic "access" {
for_each = var.dataset_access ? local.authorized_views : {}
content {
view {
project_id = each.value.project_id
dataset_id = each.value.dataset_id
table_id = each.value.table_id
}
}
}
dynamic "access" {
for_each = var.dataset_access ? local.authorized_datasets : {}
content {
dataset {
dataset {
project_id = each.value.project_id
dataset_id = each.value.dataset_id
}
target_types = ["VIEWS"]
}
}
}
dynamic "access" {
for_each = var.dataset_access ? local.authorized_routines : {}
content {
routine {
project_id = each.value.project_id
dataset_id = each.value.dataset_id
routine_id = each.value.routine_id
}
}
}
dynamic "default_encryption_configuration" {
for_each = var.encryption_key == null ? [] : [""]
content {
kms_key_name = var.encryption_key
}
}
}
resource "google_bigquery_dataset_access" "domain" {
for_each = var.dataset_access ? {} : local.access_domain
provider = google-beta
project = var.project_id
dataset_id = google_bigquery_dataset.default.dataset_id
role = each.value.role
domain = try(var.access_identities[each.key])
}
resource "google_bigquery_dataset_access" "group_by_email" {
for_each = var.dataset_access ? {} : local.access_group
provider = google-beta
project = var.project_id
dataset_id = google_bigquery_dataset.default.dataset_id
role = each.value.role
group_by_email = try(var.access_identities[each.key])
}
resource "google_bigquery_dataset_access" "special_group" {
for_each = var.dataset_access ? {} : local.access_special
provider = google-beta
project = var.project_id
dataset_id = google_bigquery_dataset.default.dataset_id
role = each.value.role
special_group = try(var.access_identities[each.key])
}
resource "google_bigquery_dataset_access" "user_by_email" {
for_each = var.dataset_access ? {} : local.access_user
provider = google-beta
project = var.project_id
dataset_id = google_bigquery_dataset.default.dataset_id
role = each.value.role
user_by_email = try(var.access_identities[each.key])
}
resource "google_bigquery_dataset_access" "authorized_views" {
for_each = var.dataset_access ? {} : local.authorized_views
project = var.project_id
dataset_id = google_bigquery_dataset.default.dataset_id
view {
project_id = each.value.project_id
dataset_id = each.value.dataset_id
table_id = each.value.table_id
}
}
resource "google_bigquery_dataset_access" "authorized_datasets" {
for_each = var.dataset_access ? {} : local.authorized_datasets
project = var.project_id
dataset_id = google_bigquery_dataset.default.dataset_id
dataset {
dataset {
project_id = each.value.project_id
dataset_id = each.value.dataset_id
}
target_types = ["VIEWS"]
}
}
resource "google_bigquery_dataset_access" "authorized_routines" {
for_each = var.dataset_access ? {} : local.authorized_routines
project = var.project_id
dataset_id = google_bigquery_dataset.default.dataset_id
routine {
project_id = each.value.project_id
dataset_id = each.value.dataset_id
routine_id = each.value.routine_id
}
}
resource "google_bigquery_dataset_iam_binding" "bindings" {
for_each = var.iam
project = var.project_id
dataset_id = google_bigquery_dataset.default.dataset_id
role = each.key
members = each.value
}
resource "google_bigquery_table" "default" {
provider = google-beta
for_each = var.tables
project = var.project_id
dataset_id = google_bigquery_dataset.default.dataset_id
table_id = each.key
friendly_name = each.value.friendly_name
description = each.value.description
clustering = each.value.options.clustering
expiration_time = each.value.options.expiration_time
labels = each.value.labels
schema = each.value.schema
deletion_protection = each.value.deletion_protection
dynamic "encryption_configuration" {
for_each = each.value.options.encryption_key != null ? [""] : []
content {
kms_key_name = each.value.options.encryption_key
}
}
dynamic "range_partitioning" {
for_each = try(each.value.partitioning.range, null) != null ? [""] : []
content {
field = each.value.partitioning.field
range {
start = each.value.partitioning.range.start
end = each.value.partitioning.range.end
interval = each.value.partitioning.range.interval
}
}
}
dynamic "time_partitioning" {
for_each = try(each.value.partitioning.time, null) != null ? [""] : []
content {
expiration_ms = each.value.partitioning.time.expiration_ms
field = each.value.partitioning.field
type = each.value.partitioning.time.type
}
}
}
resource "google_bigquery_table" "views" {
depends_on = [google_bigquery_table.default]
for_each = var.views
project = var.project_id
dataset_id = google_bigquery_dataset.default.dataset_id
table_id = each.key
friendly_name = each.value.friendly_name
description = each.value.description
labels = each.value.labels
deletion_protection = each.value.deletion_protection
view {
query = each.value.query
use_legacy_sql = each.value.use_legacy_sql
}
}

View File

@ -0,0 +1,82 @@
/**
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "dataset" {
description = "Dataset resource."
value = google_bigquery_dataset.default
}
output "dataset_id" {
description = "Dataset id."
value = google_bigquery_dataset.default.dataset_id
depends_on = [
google_bigquery_dataset_access.authorized_datasets,
google_bigquery_dataset_access.authorized_routines,
google_bigquery_dataset_access.authorized_views,
google_bigquery_dataset_access.domain,
google_bigquery_dataset_access.group_by_email,
google_bigquery_dataset_access.special_group,
google_bigquery_dataset_access.user_by_email
]
}
output "id" {
description = "Fully qualified dataset id."
value = google_bigquery_dataset.default.id
depends_on = [
google_bigquery_dataset_access.authorized_datasets,
google_bigquery_dataset_access.authorized_routines,
google_bigquery_dataset_access.authorized_views,
google_bigquery_dataset_access.domain,
google_bigquery_dataset_access.group_by_email,
google_bigquery_dataset_access.special_group,
google_bigquery_dataset_access.user_by_email
]
}
output "self_link" {
description = "Dataset self link."
value = google_bigquery_dataset.default.self_link
depends_on = [
google_bigquery_dataset_access.authorized_datasets,
google_bigquery_dataset_access.authorized_routines,
google_bigquery_dataset_access.authorized_views,
google_bigquery_dataset_access.domain,
google_bigquery_dataset_access.group_by_email,
google_bigquery_dataset_access.special_group,
google_bigquery_dataset_access.user_by_email
]
}
output "table_ids" {
description = "Map of fully qualified table ids keyed by table ids."
value = { for k, v in google_bigquery_table.default : v.table_id => v.id }
}
output "tables" {
description = "Table resources."
value = google_bigquery_table.default
}
output "view_ids" {
description = "Map of fully qualified view ids keyed by view ids."
value = { for k, v in google_bigquery_table.views : v.table_id => v.id }
}
output "views" {
description = "View resources."
value = google_bigquery_table.views
}

View File

@ -0,0 +1,173 @@
/**
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "access" {
description = "Map of access rules with role and identity type. Keys are arbitrary and must match those in the `access_identities` variable, types are `domain`, `group`, `special_group`, `user`, `view`."
type = map(object({
role = string
type = string
}))
default = {}
validation {
condition = can([
for k, v in var.access :
index(["domain", "group", "special_group", "user", "view"], v.type)
])
error_message = "Access type must be one of 'domain', 'group', 'special_group', 'user', 'view'."
}
}
variable "access_identities" {
description = "Map of access identities used for basic access roles. View identities have the format 'project_id|dataset_id|table_id'."
type = map(string)
default = {}
}
variable "authorized_datasets" {
description = "An array of datasets to be authorized on the dataset."
type = list(object({
dataset_id = string,
project_id = string,
}))
default = []
}
variable "authorized_routines" {
description = "An array of authorized routine to be authorized on the dataset."
type = list(object({
project_id = string,
dataset_id = string,
routine_id = string
}))
default = []
}
variable "authorized_views" {
description = "An array of views to be authorized on the dataset."
type = list(object({
dataset_id = string,
project_id = string,
table_id = string # this is the view id, but we keep table_id to stay consistent as the resource
}))
default = []
}
variable "dataset_access" {
description = "Set access in the dataset resource instead of using separate resources."
type = bool
default = false
}
variable "description" {
description = "Optional description."
type = string
default = "Terraform managed."
}
variable "encryption_key" {
description = "Self link of the KMS key that will be used to protect destination table."
type = string
default = null
}
variable "friendly_name" {
description = "Dataset friendly name."
type = string
default = null
}
variable "iam" {
description = "IAM bindings in {ROLE => [MEMBERS]} format. Mutually exclusive with the access_* variables used for basic roles."
type = map(list(string))
default = {}
}
variable "id" {
description = "Dataset id."
type = string
}
variable "labels" {
description = "Dataset labels."
type = map(string)
default = {}
}
variable "location" {
description = "Dataset location."
type = string
default = "EU"
}
variable "options" {
description = "Dataset options."
type = object({
default_collation = optional(string)
default_table_expiration_ms = optional(number)
default_partition_expiration_ms = optional(number)
delete_contents_on_destroy = optional(bool, false)
is_case_insensitive = optional(bool)
max_time_travel_hours = optional(number, 168)
})
default = {}
}
variable "project_id" {
description = "Id of the project where datasets will be created."
type = string
}
variable "tables" {
description = "Table definitions. Options and partitioning default to null. Partitioning can only use `range` or `time`, set the unused one to null."
type = map(object({
deletion_protection = optional(bool)
description = optional(string, "Terraform managed.")
friendly_name = optional(string)
labels = optional(map(string), {})
schema = optional(string)
options = optional(object({
clustering = optional(list(string))
encryption_key = optional(string)
expiration_time = optional(number)
}), {})
partitioning = optional(object({
field = optional(string)
range = optional(object({
end = number
interval = number
start = number
}))
time = optional(object({
expiration_ms = number
type = string
}))
}))
}))
default = {}
}
variable "views" {
description = "View definitions."
type = map(object({
query = string
deletion_protection = optional(bool)
description = optional(string, "Terraform managed.")
friendly_name = optional(string)
labels = optional(map(string), {})
use_legacy_sql = optional(bool)
}))
default = {}
}

View File

@ -0,0 +1,29 @@
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# 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.4.4"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 4.82.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
version = ">= 4.82.0" # tftest
}
}
}

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