Resource factories

This commit is contained in:
Simone Ruffilli 2021-10-14 17:00:04 +02:00
parent c67aad3bb8
commit 36fb785ea9
56 changed files with 1303 additions and 4 deletions

View File

@ -43,11 +43,13 @@ steps:
args:
[
"/workspace/tools/check_documentation.py",
"modules",
"cloud-operations",
"data-solutions",
"data-solutions/data-platform-foundations",
"factories",
"factories/firewall-vpc-rules"
"foundations",
"modules",
"networking",
]

View File

@ -32,6 +32,7 @@ steps:
- -vv
- tests/cloud_operations
- tests/data_solutions
- tests/factories
- tests/foundations
- tests/networking
env:

40
factories/README.md Normal file
View File

@ -0,0 +1,40 @@
# Resource Factories
This set of modules creates specialized resource factories, made of two distinct components:
- a module, which implements the factory logic in Terraform syntax, and
- a set of directories, which hold the configuration for the factory in YAML syntax.
## Available modules
- [Hierarchical Firewall policies](./firewall-hierarchical-policies)
- [VPC Firewall rules](./firewall-vpc-rules)
- [Subnets](./subnets)
## Example implementation
See [Example environments](./example-environments)
## Using the modules
Each module is specialised and comes with a `README.md` file which describes the module interface, as well as the directory structure each module requires.
## Rationale
This approach is based on modules implementing the factory logic using Terraform code and a set of directories having a well-defined, semantic structure, holding the configuration for the resources in YaML syntax.
Resource factories are designed to:
- accelerate and rationalize the repetitive creation of common resources, such as firewall rules and subnets.
- enable teams without Terraform specific knowledge to build IaC leveraging human-friendly and machine-parseable YAML files
- make it simple to implement specific requirements and best practices (e.g. "always enable PGA for GCP subnets", or "only allow using regions `europe-west1` and `europe-west3`")
- codify and centralise business logics and policies (e.g. labels and naming conventions)
Terraform natively supports YaML, JSON and CSV parsing - however we've decided to embrace YaML for the following reasons:
- YaML is easier to parse for a human, and allows for comments and nested, complex structures
- JSON and CSV can't include comments, which can be used to document configurations, but are often useful to bridge from other systems in automated pipelines
- JSON is more verbose (reads: longer) and harder to parse for a human
- CSV isn't often expressive enough (e.g. doesn't allow for nested structures)
If needed, converting factories to consume JSON instead is a matter of switching from `yamldecode()` to `jsondecode()` in the right place on each module.

View File

@ -0,0 +1,42 @@
# Resource Factories
The example in this folder are derived from actual production use cases, and show how to use a factory module and how you could structure your codebase for multiple environments.
## Resource Factories usage - Managing subnets
At the top level of this directory, besides the `README.md` your're reading now, you'll find
- `dev/`, a directory which holds all configurations for the *development* environment
- `prod/`, a directory which holds all configurations for the *production* environment
- `main.tf`, a simple terraform file which consumes the [`subnets`](../subnets/) module
Each environment directory structure is meant to mimic your GCP resources structure
```
.
├── dev # Environment
│ ├── project-dev-a # Project id
│ │ └── vpc-alpha # VPC name
│ │ ├── subnet-alpha-a.yaml # Subnet name (one file per subnet)
│ │ └── subnet-alpha-b.yaml
│ └── project-dev-b
│ ├── vpc-beta
│ │ └── subnet-beta-a.yaml
│ └── vpc-gamma
│ └── subnet-gamma-a.yaml
├── prod
│ └── project-prod-a
│ └── vpc-alpha
│ ├── subnet-alpha-a.yaml
│ └── subnet-alpha-b.yaml
├── main.tf
└── README.md
```
Since this resource factory only creates subnets, projects and VPCs are expected to exist.
In this example, a single `main.tf` file (hence a single state) drives the creation of both the `dev` and the `prod` environment. Another option you might want to consider, in line with the CI/CD pipeline or processes you have in place, might be to move the `main.tf` to the each environment directory, so that states (and pipelines) can be separated.

View File

@ -0,0 +1,6 @@
region: europe-west3
ip_cidr_range: 10.0.0.0/24
description: Sample Subnet in project project-dev-a, vpc-alpha
secondary_ip_ranges:
secondary-range-a: 192.168.0.0/24
secondary-range-b: 192.168.1.0/24

View File

@ -0,0 +1,4 @@
region: europe-west3
ip_cidr_range: 10.0.1.0/24
description: Sample Subnet in project project-dev-a, vpc-alpha
private_ip_google_access: false

View File

@ -0,0 +1,5 @@
region: europe-west4
ip_cidr_range: 10.0.2.0/24
description: Sample Subnet in project project-dev-b, vpc-beta
iam_users: ["sruffilli@google.com"]
iam_groups: []

View File

@ -0,0 +1,3 @@
region: europe-west4
ip_cidr_range: 10.0.3.0/24
description: Sample Subnet in project project-dev-b, vpc-gamma

View File

@ -0,0 +1,26 @@
/**
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module "subnets-dev" {
source = "../subnets"
config_folder = "dev"
}
module "subnets-prod" {
source = "../subnets"
config_folder = "prod"
}

View File

@ -0,0 +1,6 @@
region: europe-west3
ip_cidr_range: 10.0.0.0/24
description: Sample Subnet in project project-prod-a, vpc-alpha
secondary_ip_ranges:
secondary-range-a: 192.168.0.0/24
secondary-range-b: 192.168.1.0/24

View File

@ -0,0 +1,4 @@
region: europe-west3
ip_cidr_range: 10.0.1.0/24
description: Sample Subnet in project project-prod-a, vpc-alpha
private_ip_google_access: false

View File

@ -0,0 +1,161 @@
# Google Cloud Resource Factories - Hierarchical Firewall Policies
This module implements a resource factory which allows the creation and management of [hierarchical firewall policies](https://cloud.google.com/vpc/docs/firewall-policies) through properly formatted `yaml` files.
`yaml` configurations are stored on a well-defined folder structure, whose entry point can be customized, and which allows for simple grouping of policies by Organization ID.
This module also allows for a definition of variable templates, allowing for the definition and centralization of common CIDRs or Service Account lists, which enables re-using them across different policies.
## Example
### Terraform code
```hcl
module "hierarchical" {
source = "./modules/resource-factories/hierarchical-firewall"
config_folder = "firewall/hierarchical"
templates_folder = "firewall/templates"
}
# tftest:skip
```
### Configuration Structure
The naming convention for the `config_folder` folder requires
- the first directory layer to be named after the organization ID we're creating the policies for
- each file to be either named either `$folder_id-$description.yaml` (e.g. `1234567890-sharedinfra.yaml`) for policies applying to regular folders or `org.yaml` for the root folder.
Organizations and folders should exist prior to running this module, or set as an explicit dependency to this module, leveraging `depends_on`.
The optional `templates_folder` folder can have two files.
- `cidrs.yaml` - a YAML map defining lists of CIDRs
- `service_accounts.yaml` - a YAML map definint lists of Service Accounts
Examples for both files are shown in the following section.
```bash
└── firewall
├── hierarchical
│ ├── 31415926535
│ │ ├── 1234567890-sharedinfra.yaml # Maps to folders/1234567890
│ │ └── org.yaml # Maps to organizations/31415926535
│ └── 27182818284
│ └── 1234567891-sharedinfra.yaml # Maps to folders/1234567891
└── templates
├── cidrs.yaml
└── service_accounts.yaml
```
### Hierarchical firewall policies format and structure
The following syntax applies both for `$folder_id-$description.yaml` and for `org.yaml` files, with the former applying at the `$folder_id` level and the latter at the Organization level.
Each file can contain an arbitrary number of policies.
```yaml
# Policy name
allow-icmp:
# Description
description: Sample policy
# Direction {INGRESS, EGRESS}
direction: INGRESS
# Action {allow, deny}
action: allow
# Priority (must be unique on a node)
priority: 1000
# List of CIDRs this rule applies to
source_ranges:
- 0.0.0.0/0
# List of ports this rule applies to (empty array means all ports)
ports:
tcp: []
udp: []
icmp: []
# List of VPCs this rule applies to - a null value implies all VPCs
target_resources: null
# Opt - List of target Service Accounts this rule applies to
target_service_accounts:
- example-service-account@foobar.iam.gserviceaccount.com
# Opt - Whether to enable logs - defaults to false
enable_logging: true
```
A sample configuration file might look like the following one:
```yaml
allow-icmp:
description: Enable ICMP for all hosts
direction: INGRESS
action: allow
priority: 1000
source_ranges:
- 0.0.0.0/0
ports:
icmp: []
target_resources: null
enable_logging: false
allow-ssh-from-onprem:
description: Enable SSH for on prem hosts
direction: INGRESS
action: allow
priority: 1001
source_ranges:
- $onprem
ports:
tcp: ["22"]
target_resources: null
enable_logging: false
allow-443-from-clients:
description: Enable HTTPS for web clients
direction: INGRESS
action: allow
priority: 1001
source_ranges:
- $web_clients
ports:
tcp: ["443"]
target_resources: null
target_service_accounts:
- $web_frontends
enable_logging: false
```
with `firewall/templates/cidrs.yaml` defined as follows:
```yaml
onprem:
- 10.0.0.0/8
- 192.168.0.0/16
web_clients:
- 172.16.0.0/16
- 10.0.10.0/24
- 10.0.250.0/24
```
and `firewall/templates/service_accounts.yaml`:
```yaml
web_frontends:
- web-frontends@project-wf1.iam.gserviceaccount.com
- web-frontends@project-wf2.iam.gserviceaccount.com
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---: |:---:|:---:|
| config_folder | Relative path of the folder containing the hierarchical firewall configuration | <code title="">string</code> | ✓ | |
| templates_folder | Relative path of the folder containing the cidr/service account templates | <code title="">string</code> | ✓ | |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| hierarchical-firewall-rules | Generated Hierarchical Firewall Rules | |
<!-- END TFDOC -->

View File

@ -0,0 +1,101 @@
/**
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 {
cidrs = try({ for name, cidrs in yamldecode(file("${var.templates_folder}/cidrs.yaml")) :
name => cidrs
}, {})
service_accounts = try({ for name, service_accounts in yamldecode(file("${var.templates_folder}/service_accounts.yaml")) :
name => service_accounts
}, {})
org_paths = {
for file in fileset(var.config_folder, "**/*.yaml") :
file => split("/", file)[1] == "org.yaml"
? "organizations/${split("/", file)[0]}"
: "folders/${split("-", split("/", file)[1])[0]}"
}
rules = flatten([
for file in fileset(var.config_folder, "**/*.yaml") : [
for key, ruleset in yamldecode(file("${var.config_folder}/${file}")) :
merge(ruleset, {
parent_id = local.org_paths[file]
name = "${replace(local.org_paths[file], "/", "-")}-${key}"
source_ranges = try(ruleset.source_ranges, null) == null ? null : flatten(
[for cidr in ruleset.source_ranges :
can(regex("^\\$", cidr))
? local.cidrs[trimprefix(cidr, "$")]
: [cidr]
])
destination_ranges = try(ruleset.destination_ranges, null) == null ? null : flatten(
[for cidr in ruleset.destination_ranges :
can(regex("^\\$", cidr))
? local.cidrs[trimprefix(cidr, "$")]
: [cidr]
])
target_service_accounts = try(ruleset.target_service_accounts, null) == null ? null : flatten(
[for service_account in ruleset.target_service_accounts :
can(regex("^\\$", service_account))
? local.service_accounts[trimprefix(service_account, "$")]
: [service_account]
])
})
]
])
}
resource "google_compute_organization_security_policy" "default" {
provider = google-beta
for_each = { for rule in local.rules : rule.parent_id => rule.name... }
display_name = replace("hierarchical-fw-policy-${each.key}", "/", "-")
parent = each.key
}
resource "google_compute_organization_security_policy_rule" "default" {
provider = google-beta
for_each = { for rule in local.rules : "${rule.parent_id}-${rule.name}" => rule }
policy_id = google_compute_organization_security_policy.default[each.value.parent_id].id
action = each.value.action
direction = each.value.direction
priority = each.value.priority
target_resources = each.value.target_resources
target_service_accounts = each.value.target_service_accounts
enable_logging = try(each.value.enable_logging, false)
# preview = each.value.preview
match {
config {
src_ip_ranges = each.value.source_ranges
dynamic "layer4_config" {
for_each = each.value.ports
iterator = port
content {
ip_protocol = port.key
ports = port.value
}
}
}
}
}
resource "google_compute_organization_security_policy_association" "default" {
provider = google-beta
for_each = { for rule in local.rules : rule.parent_id => rule.name... }
name = replace("hierarchical-fw-policy-${each.key}", "/", "-")
attachment_id = google_compute_organization_security_policy.default[each.key].parent
policy_id = google_compute_organization_security_policy.default[each.key].id
}

View File

@ -0,0 +1,27 @@
/**
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "hierarchical-firewall-rules" {
description = "Generated Hierarchical Firewall Rules"
value = {
for k, v in google_compute_organization_security_policy_rule.default :
k => {
parent_id = split("-", k)[0]
id = v.id
description = v.match[0].description
}
}
}

View File

@ -0,0 +1,9 @@
variable "config_folder" {
description = "Relative path of the folder containing the hierarchical firewall configuration"
type = string
}
variable "templates_folder" {
description = "Relative path of the folder containing the cidr/service account templates"
type = string
}

View File

@ -0,0 +1,6 @@
# Google Cloud VPC Firewall Factories
This collection of modules implement two different metodologies for the creation of VPC firewall rules, both based on leveraging well-defined `yaml` configuration files.
- The [flat module](flat/) delegates the definition of all firewall rules metadata (project, network amongst other) to the individual `yaml` configuration. This module allows for the maximum flexibility, and a custom logical grouping of resources which can be trasversal to the traditional resources hierarchy, and could be useful in scenarios where network is not managed centrally by a single team.
- The [nested module](nested/) requires and enforces a semantical folder structure that carries some of the rules metadata (project and network), and leaves the rest to each `yaml` configuration. This solution allows for the definition of a resource hierarchy that is aligned with the organisational resource structure.

View File

@ -1,10 +1,10 @@
# Google Cloud VPC Firewall - Yaml
# Google Cloud VPC Firewall Factory - Flat hierarchy
This module allows creation and management of different types of firewall rules by defining them in well formatted `yaml` files.
Yaml abstraction for FW rules can simplify users onboarding and also makes rules definition simpler and clearer comparing to HCL.
Nested folder structure for yaml configurations is supported, which allows better and structured code management for multiple teams and environments.
Nested folder structure for yaml configurations is optionally supported, which allows better and structured code management for multiple teams and environments.
## Example

View File

@ -0,0 +1,140 @@
# Google Cloud VPC Firewall Factory - Nested hierarchy
This module implements a resource factory which allows the creation and management of [VPC firewall rules](https://cloud.google.com/vpc/docs/firewalls) through properly formatted `yaml` files.
`yaml` configurations are stored on a well-defined folder structure, whose entry point can be customized, and which represents and forces the resource hierarchy a firewall rule belongs to (Project > VPC > Firewall Rule).
This module also allows for a definition of variable templates, allowing for the definition and centralization of common CIDRs or Service Account lists, which enables re-using them across different policies.
## Example
### Terraform code
```hcl
module "vpc-firewall" {
source = "../../cloud-foundation-fabric/modules/resource-factories/vpc-firewall"
config_folder = "firewall/vpc"
templates_folder = "firewall/templates"
}
# tftest:skip
```
### Configuration Structure
The naming convention for the `config_folder` folder **requires**
- the first directory layer to be named after the project ID which contains the VPC we're creating the firewall rules for
- the second directory layer to be named after the VPC we're creating the firewall rules for
- `yaml` files contained in the "VPC" directory can be arbitrarily named, to allow for an easier logical grouping.
Projects and VPCs should exist prior to running this module, or set as an explicit dependency to this module, leveraging `depends_on`.
The optional `templates_folder` folder can have two files.
- `cidrs.yaml` - a YAML map defining lists of CIDRs
- `service_accounts.yaml` - a YAML map definint lists of Service Accounts
```bash
└── firewall
├── vpc
│ ├── project-resource-factory-dev
│ │ └── vpc-resource-factory-dev-one
│ │ │ ├── frontend.yaml
│ │ │ └── backend.yaml
│ │ └── vpc-resource-factory-dev-two
│ │ ├── foo.yaml
│ │ └── bar.yaml
│ └── project-resource-factory-prod
│ │ └── vpc-resource-factory-prod-alpha
│ │ ├── lorem.yaml
│ │ └── ipsum.yaml
└── templates
├── cidrs.yaml
└── service_accounts.yaml
```
### Rule definition format and structure
Firewall rules configuration should be placed in a set of yaml files in a folder/s. Firewall rule entry structure is following:
```yaml
rule-name: # descriptive name, naming convention is adjusted by the module
description: "Allow icmp" # rule description
action: allow # `allow` or `deny`
direction: INGRESS # EGRESS or INGRESS
ports:
icmp: [] # {tcp, udp, icmp, all}: [ports], use [] for any port
priority: 1000 # rule priority value, default value is 1000
source_ranges: # list of source ranges
- 0.0.0.0/0
destination_ranges: # list of destination ranges
- 0.0.0.0/0
source_tags: ['some-tag'] # list of source tags
source_service_accounts: # list of source service accounts
- myapp@myproject-id.iam.gserviceaccount.com
target_tags: ['some-tag'] # list of target tags
target_service_accounts: # list of target service accounts
- myapp@myproject-id.iam.gserviceaccount.com
enable_logging: true # `false` or `true`, logging is enabled when `true`
```
A sample configuration file might look like the following one:
```yaml
allow-healthchecks:
description: "Allow traffic from healthcheck"
direction: INGRESS
action: allow
priority: 1000
source_ranges:
- $healthcheck
ports:
tcp: ["80"]
enable_logging: false
allow-http:
description: "Allow traffic to LB backend"
direction: INGRESS
action: allow
priority: 1000
source_ranges:
- 0.0.0.0/0
target_service_accounts:
- $web_frontends
ports:
tcp: ["80", "443"]
enable_logging: false
```
with `firewall/templates/cidrs.yaml` defined as follows:
```yaml
healthcheck:
- 35.191.0.0/16
- 130.211.0.0/22
```
and `firewall/templates/service_accounts.yaml`:
```yaml
web_frontends:
- web-frontends@project-wf1.iam.gserviceaccount.com
- web-frontends@project-wf2.iam.gserviceaccount.com
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---: |:---:|:---:|
| config_folder | Relative path of the folder containing the hierarchical firewall configuration | <code title="">string</code> | ✓ | |
| templates_folder | Relative path of the folder containing the cidr/service account templates | <code title="">string</code> | ✓ | |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| vpc-firewall-rules | Generated VPC Firewall Rules | |
<!-- END TFDOC -->

View File

@ -0,0 +1,151 @@
/**
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 {
cidrs = try({ for name, cidrs in yamldecode(file("${var.templates_folder}/cidrs.yaml")) :
name => cidrs
}, {})
service_accounts = try({ for name, service_accounts in yamldecode(file("${var.templates_folder}/service_accounts.yaml")) :
name => service_accounts
}, {})
rules = flatten([
for file in fileset(var.config_folder, "**/*.yaml") : [
for key, ruleset in yamldecode(file("${var.config_folder}/${file}")) :
merge(ruleset, {
project_id = split("/", file)[0]
network = split("/", file)[1]
name = "${key}-${split("/", file)[1]}"
source_ranges = try(ruleset.source_ranges, null) == null ? null : flatten(
[for cidr in ruleset.source_ranges :
can(regex("^\\$", cidr))
? local.cidrs[trimprefix(cidr, "$")]
: [cidr]
])
destination_ranges = try(ruleset.destination_ranges, null) == null ? null : flatten(
[for cidr in ruleset.destination_ranges :
can(regex("^\\$", cidr))
? local.cidrs[trimprefix(cidr, "$")]
: [cidr]
])
source_service_accounts = try(ruleset.source_service_accounts, null) == null ? null : flatten(
[for service_account in ruleset.source_service_accounts :
can(regex("^\\$", service_account))
? local.service_accounts[trimprefix(service_account, "$")]
: [service_account]
])
target_service_accounts = try(ruleset.target_service_accounts, null) == null ? null : flatten(
[for service_account in ruleset.target_service_accounts :
can(regex("^\\$", service_account))
? local.service_accounts[trimprefix(service_account, "$")]
: [service_account]
])
})
]
])
rules_allow = [for item in local.rules : item if item.action == "allow"]
rules_deny = [for item in local.rules : item if item.action == "deny"]
}
resource "google_compute_firewall" "rules-allow" {
for_each = { for rule in local.rules_allow : "${rule.network}-${rule.name}" => rule }
project = each.value.project_id
name = each.value.name
description = each.value.description
network = each.value.network
direction = each.value.direction
priority = each.value.priority
source_ranges = try(each.value.source_ranges, each.value.direction == "INGRESS" ? [] : null)
source_tags = try(each.value.source_tags, null)
source_service_accounts = try(each.value.source_service_accounts, null)
destination_ranges = try(each.value.destination_ranges, each.value.direction == "EGRESS" ? [] : null)
target_tags = try(each.value.target_tags, null)
target_service_accounts = try(each.value.target_service_accounts, null)
dynamic "allow" {
for_each = { for proto, ports in try(each.value.ports, []) :
"${proto}-${join("-", ports)}" => {
ports = [for port in ports : tostring(port)]
protocol = proto
}
}
content {
protocol = allow.value.protocol
ports = allow.value.ports
}
}
dynamic "log_config" {
for_each = (each.value.enable_logging == null) || (each.value.enable_logging == false) ? [] : [""]
content {
metadata = "INCLUDE_ALL_METADATA"
}
}
lifecycle {
create_before_destroy = true
}
}
resource "google_compute_firewall" "rules-deny" {
for_each = { for rule in local.rules_deny : "${rule.network}-${rule.name}" => rule }
project = each.value.project_id
name = each.value.name
description = each.value.description
network = each.value.network
direction = each.value.direction
priority = each.value.priority
source_ranges = try(each.value.source_ranges, each.value.direction == "INGRESS" ? [] : null)
source_tags = try(each.value.source_tags, null)
source_service_accounts = try(each.value.source_service_accounts, null)
destination_ranges = try(each.value.destination_ranges, each.value.direction == "EGRESS" ? [] : null)
target_tags = try(each.value.target_tags, null)
target_service_accounts = try(each.value.target_service_accounts, null)
dynamic "deny" {
for_each = { for proto, ports in try(each.value.ports, []) :
"${proto}-${join("-", ports)}" => {
ports = [for port in ports : tostring(port)]
protocol = proto
}
}
content {
protocol = deny.value.protocol
ports = deny.value.ports
}
}
dynamic "log_config" {
for_each = (each.value.enable_logging == null) || (each.value.enable_logging == false) ? [] : [""]
content {
metadata = "INCLUDE_ALL_METADATA"
}
}
lifecycle {
create_before_destroy = true
}
}

View File

@ -0,0 +1,4 @@
output "vpc-firewall-rules" {
description = "Generated VPC Firewall Rules"
value = merge(google_compute_firewall.rules-allow, google_compute_firewall.rules-deny)
}

View File

@ -0,0 +1,9 @@
variable "config_folder" {
description = "Relative path of the folder containing the hierarchical firewall configuration"
type = string
}
variable "templates_folder" {
description = "Relative path of the folder containing the cidr/service account templates"
type = string
}

View File

@ -0,0 +1,68 @@
# Google Cloud Resource Factories - VPC Subnets
This module implements a resource factory which allows the creation and management of subnets through properly formatted `yaml` files.
`yaml` configurations are stored on a well-defined folder structure, whose entry point can be customized, and which allows for simple grouping of subnets by Project > VPC.
## Example
### Terraform code
```hcl
module "subnets" {
source = "./modules/resource-factories/subnets"
config_folder = "subnets"
}
# tftest:skip
```
### Configuration Structure
The directory structure implies the project and the VPC each subnet belongs to.
Per the structure below, a subnet named `subnet-a` (after filename `subnet-a.yaml`) will be created on VPC `vpc-alpha-one` which belongs to project `project-alpha`.
Projects and VPCs should exist prior to running this module, or set as an explicit dependency to this module, leveraging `depends_on`.
```bash
└── subnets
├── project-alpha
│ ├── vpc-alpha-one
│ │ ├── subnet-a.yaml
│ │ └── subnet-b.yaml
│ └── vpc-alpha-two
│ └── subnet-c.yaml
└── project-bravo
└── vpc-bravo-one
└── subnet-d.yaml
```
### Subnet definition format and structure
```yaml
region: europe-west1 # Region where the subnet will be creted
description: Sample description # Description
ip_cidr_range: 10.0.0.0/24 # Primary IP range for the subnet
private_ip_google_access: false # Opt- Enables PGA. Defaults to true
iam_users: ["foobar@example.com"] # Opt- Users to grant compute/networkUser to
iam_groups: ["lorem@example.com"] # Opt- Groups to grant compute/networkUser to
iam_service_accounts: ["foobar@project-id.iam.gserviceaccount.com"]
# Opt- SAs to grant compute/networkUser to
secondary_ip_ranges: # Opt- List of secondary IP ranges
- name: secondary-range-a # Name for the secondary range
ip_cidr_range: 192.168.0.0/24 # IP range for the secondary range
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---: |:---:|:---:|
| config_folder | Relative path of the folder containing the subnet configuration | <code title="">string</code> | ✓ | |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| subnet | Generated subnets | |
<!-- END TFDOC -->

72
factories/subnets/main.tf Normal file
View File

@ -0,0 +1,72 @@
/**
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
locals {
_data = {
for f in fileset(var.config_folder, "**/*.yaml") :
trimsuffix(split("/", f)[2], ".yaml") => merge(
yamldecode(file("${var.config_folder}/${f}")),
{
project_id = split("/", f)[0]
network = split("/", f)[1]
}
)
}
data = {
for k, v in local._data : k => merge(v,
{
network_users : concat(
formatlist("group:%s", try(v.iam_groups, [])),
formatlist("user:%s", try(v.iam_users, [])),
formatlist("serviceAccount:%s", try(v.iam_service_accounts, []))
)
}
)
}
}
resource "google_compute_subnetwork" "default" {
for_each = local.data
project = each.value.project_id
network = each.value.network
name = each.key
region = each.value.region
description = each.value.description
ip_cidr_range = each.value.ip_cidr_range
private_ip_google_access = try(each.value.private_ip_google_access, true)
dynamic "secondary_ip_range" {
for_each = try(each.value.secondary_ip_ranges, [])
iterator = secondary_range
content {
range_name = secondary_range.key
ip_cidr_range = secondary_range.value
}
}
}
resource "google_compute_subnetwork_iam_binding" "default" {
for_each = {
for k, v in local.data : k => v if length(v.network_users) > 0
}
project = each.value.project_id
subnetwork = google_compute_subnetwork.default[each.key].name
region = each.value.region
role = "roles/compute.networkUser"
members = each.value.network_users
}

View File

@ -0,0 +1,28 @@
/**
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "subnet" {
description = "Generated subnets"
value = {
for k, v in google_compute_subnetwork.default :
k => {
network = v.network
project = v.project
range = v.ip_cidr_range
region = v.region
}
}
}

View File

@ -0,0 +1,20 @@
/**
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "config_folder" {
description = "Relative path of the folder containing the subnet configuration"
type = string
}

View File

@ -0,0 +1,13 @@
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

View File

@ -0,0 +1,11 @@
allow-ssh-from-onprem:
description: Enable SSH for onprem ranges
direction: INGRESS
action: allow
priority: 1001
source_ranges:
- $example
ports:
tcp: ["22"]
target_resources: null
enable_logging: false

View File

@ -0,0 +1,11 @@
allow-icmp:
description: Enable ICMP for all hosts
direction: INGRESS
action: allow
priority: 1000
source_ranges:
- 0.0.0.0/0
ports:
icmp: []
target_resources: null
enable_logging: false

View File

@ -0,0 +1,8 @@
example:
- 10.0.0.0/24
- 10.0.10.0/24
- 192.168.1.1/32
healthcheck:
- 35.191.0.0/16
- 130.211.0.0/22

View File

@ -0,0 +1,2 @@
example:
- example-service-account@resource-factory-playground.iam.gserviceaccount.com

View File

@ -0,0 +1,21 @@
/**
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module "hierarchical-firewall-rules" {
source = "../../../../factories/firewall-hierarchical-policies/"
config_folder = "conf/rules"
templates_folder = "conf/templates"
}

View File

@ -0,0 +1,43 @@
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import pytest
FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixture")
def test_firewall(plan_runner):
"Test hierarchical firewall rules from conf/rules"
_, resources = plan_runner(FIXTURES_DIR)
assert len(resources) == 6
assert set(r["type"] for r in resources) == set([
"google_compute_organization_security_policy_rule", "google_compute_organization_security_policy_association", "google_compute_organization_security_policy"
])
rule_ssh = [r["values"] for r in resources if r["type"]== "google_compute_organization_security_policy_rule" and r["values"]["priority"]==1001]
rule_icmp = [r["values"] for r in resources if r["type"]== "google_compute_organization_security_policy_rule" and r["values"]["priority"]==1000]
association_org = [r["values"] for r in resources if r["type"]== "google_compute_organization_security_policy_association" and r["values"]["attachment_id"]=="organizations/1234567890"]
association_folder = [r["values"] for r in resources if r["type"]== "google_compute_organization_security_policy_association" and r["values"]["attachment_id"]=="folders/0987654321"]
policies_org = [r["values"] for r in resources if r["type"]== "google_compute_organization_security_policy" and r["values"]["parent"]=="organizations/1234567890"]
policies_folder = [r["values"] for r in resources if r["type"]== "google_compute_organization_security_policy" and r["values"]["parent"]=="folders/0987654321"]
assert set(rule_ssh[0]["match"][0]["config"][0]["src_ip_ranges"])==set(["10.0.0.0/24", "10.0.10.0/24", "192.168.1.1/32"])
assert rule_icmp[0]["match"][0]["config"][0]["layer4_config"][0]["ip_protocol"]=="icmp"
assert association_org[0]["name"]=="hierarchical-fw-policy-organizations-1234567890"
assert association_folder[0]["name"]=="hierarchical-fw-policy-folders-0987654321"
assert policies_org[0]["display_name"]=="hierarchical-fw-policy-organizations-1234567890"
assert policies_folder[0]["display_name"]=="hierarchical-fw-policy-folders-0987654321"

View File

@ -0,0 +1,13 @@
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

View File

@ -0,0 +1,6 @@
region: europe-west1
ip_cidr_range: 10.0.0.0/24
description: Sample Subnet in project project-a, vpc-a
secondary_ip_ranges:
secondary-range-a: 192.168.0.0/24
secondary-range-b: 192.168.1.0/24

View File

@ -0,0 +1,4 @@
region: europe-west3
ip_cidr_range: 10.0.1.0/24
description: Sample Subnet in project project-a, vpc-a
private_ip_google_access: false

View File

@ -0,0 +1,5 @@
region: europe-west4
ip_cidr_range: 10.0.2.0/24
description: Sample Subnet in project project-a, vpc-b
iam_users: ["sruffilli@google.com"]
iam_groups: []

View File

@ -0,0 +1,5 @@
region: europe-west4
ip_cidr_range: 172.16.0.0/24
description: Sample Subnet in project project-b, vpc-x
iam_users: ["sruffilli@google.com"]
iam_groups: []

View File

@ -0,0 +1,20 @@
/**
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module "subnets" {
source = "../../../../factories/subnets"
config_folder = "conf"
}

View File

@ -0,0 +1,64 @@
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import pytest
FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixture")
def test_firewall(plan_runner):
"Test hierarchical firewall rules from conf/rules"
_, resources = plan_runner(FIXTURES_DIR)
assert len(resources) == 6
assert set(r["type"] for r in resources) == set(
["google_compute_subnetwork", "google_compute_subnetwork_iam_binding"])
subnets = [
r["values"] for r in resources
if r["type"] == "google_compute_subnetwork"
]
iam_bindings = [
r["values"] for r in resources
if r["type"] == "google_compute_subnetwork_iam_binding"
]
subnet_a_a = [
s for s in subnets
if s["project"] == "project-a" and s["network"] == "vpc-a" and s["name"] == "subnet-a"
][0]
assert subnet_a_a["ip_cidr_range"] == "10.0.0.0/24"
assert subnet_a_a["private_ip_google_access"] == True
assert subnet_a_a["region"] == "europe-west1"
assert subnet_a_a["secondary_ip_range"] == [{
"ip_cidr_range":
"192.168.0.0/24",
"range_name":
"secondary-range-a"
}, {
"ip_cidr_range":
"192.168.1.0/24",
"range_name":
"secondary-range-b"
}]
subnet_a_b = [
s for s in subnets
if s["project"] == "project-a" and s["network"] == "vpc-a" and s["name"] == "subnet-b"
][0]
assert subnet_a_b["private_ip_google_access"] == False
iam_binding_b_alpha = [b for b in iam_bindings if b["project"]=="project-b"][0]
assert set(iam_binding_b_alpha["members"])==set(["user:sruffilli@google.com"])
assert iam_binding_b_alpha["role"]=="roles/compute.networkUser"
assert iam_binding_b_alpha["subnetwork"]=="subnet-alpha"

View File

@ -0,0 +1,13 @@
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

View File

@ -0,0 +1,13 @@
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

View File

@ -15,7 +15,7 @@
*/
module "firewall" {
source = "../../../../modules/net-vpc-firewall-yaml"
source = "../../../../../factories/firewall-vpc-rules/flat"
project_id = "my-project"
network = "my-network"
config_directories = [

View File

@ -0,0 +1,13 @@
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

View File

@ -0,0 +1,23 @@
allow-healthchecks:
description: "Allow traffic from healthcheck"
direction: INGRESS
action: allow
priority: 1000
source_ranges:
- $healthcheck
ports:
tcp: ["80"]
enable_logging: false
allow-http:
description: "Allow traffic to LB backend"
direction: INGRESS
action: allow
priority: 1000
source_ranges:
- 0.0.0.0/0
target_service_accounts:
- example-service-account@resource-factory-playground.iam.gserviceaccount.com
ports:
tcp: ["80", "443"]
enable_logging: true

View File

@ -0,0 +1,8 @@
example:
- 10.0.0.0/24
- 10.0.10.0/24
- 192.168.1.1/32
healthcheck:
- 35.191.0.0/16
- 130.211.0.0/22

View File

@ -0,0 +1,2 @@
couchbase:
- example-service-account@resource-factory-playground.iam.gserviceaccount.com

View File

@ -0,0 +1,21 @@
/**
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module "vpc-firewall-rules" {
source = "../../../../../factories/firewall-vpc-rules/nested"
config_folder = "conf/rules"
templates_folder = "conf/templates"
}

View File

@ -0,0 +1,45 @@
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import pytest
FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixture")
def test_firewall(plan_runner):
"Test hierarchical firewall rules from conf/rules"
_, resources = plan_runner(FIXTURES_DIR)
assert len(resources) == 2
assert set(r["type"]
for r in resources) == set(["google_compute_firewall"])
rule_hc = [
r["values"] for r in resources
if r["values"]["name"] == "allow-healthchecks-vpc-a"
][0]
rule_be = [
r["values"] for r in resources
if r["values"]["description"] == "Allow traffic to LB backend"
][0]
assert set(rule_hc["source_ranges"]) == set(
["130.211.0.0/22", "35.191.0.0/16"])
assert rule_hc["direction"] == "INGRESS"
assert rule_hc["network"] == "vpc-a"
assert rule_hc["priority"] == 1000
assert rule_hc["project"] == "resource-factory-playground"
assert rule_hc["allow"][0] == {'ports': ['80'], 'protocol': 'tcp'}
assert rule_be["log_config"][0] == {'metadata': 'INCLUDE_ALL_METADATA'}