diff --git a/.ci/.terraformrc b/.ci/.terraformrc new file mode 100644 index 000000000..6c09c5628 --- /dev/null +++ b/.ci/.terraformrc @@ -0,0 +1 @@ +plugin_cache_dir = "/workspace/.terraform.d/plugin-cache" \ No newline at end of file diff --git a/.ci/cloudbuild.lint.yaml b/.ci/cloudbuild.lint.yaml index c88a13b34..9fb35a96e 100644 --- a/.ci/cloudbuild.lint.yaml +++ b/.ci/cloudbuild.lint.yaml @@ -14,12 +14,12 @@ steps: - - name: "python:3-alpine" - id: "boilerplate" + - name: python:3-alpine + id: boilerplate args: ["/workspace/.ci/scripts/check_boilerplate.py", "/workspace"] - - name: "wata727/tflint" - id: "lint" + - name: wata727/tflint + id: lint args: ["/workspace"] tags: - - "ci" - - "lint" + - ci + - lint diff --git a/.ci/cloudbuild.test.yaml b/.ci/cloudbuild.test.yaml index 24de3dfad..d248a3cb2 100644 --- a/.ci/cloudbuild.test.yaml +++ b/.ci/cloudbuild.test.yaml @@ -23,28 +23,21 @@ steps: wget https://releases.hashicorp.com/terraform/${_TERRAFORM_VERSION}/terraform_${_TERRAFORM_VERSION}_linux_amd64.zip && unzip terraform_${_TERRAFORM_VERSION}_linux_amd64.zip -d /builder/home/.local/bin && rm terraform_${_TERRAFORM_VERSION}_linux_amd64.zip && - chmod 755 /builder/home/.local/bin/terraform - # TODO(ludoo): split into two triggers with different filters + chmod 755 /builder/home/.local/bin/terraform && + mkdir -p /workspace/.terraform.d/plugin-cache + # TODO(ludoo): add a step that detects change files and sets tests to run - name: python:3-alpine - id: test-foundations + id: test-modules entrypoint: pytest args: - -v - - tests/foundations + - tests/modules env: - PATH=/usr/local/bin:/usr/bin:/bin:/builder/home/.local/bin - - name: python:3-alpine - id: test-infrastructure - entrypoint: pytest - args: - - -v - - tests/infrastructure - env: - - PATH=/usr/local/bin:/usr/bin:/bin:/builder/home/.local/bin - - PYTHONDONTWRITEBYTECODE=true + - TF_CLI_CONFIG_FILE=/workspace/.ci/.terraformrc substitutions: - _TERRAFORM_VERSION: 0.12.19 + _TERRAFORM_VERSION: 0.12.20 tags: - "ci" diff --git a/.ci/scripts/check_boilerplate.py b/.ci/scripts/check_boilerplate.py index 762d67d76..a3bfa03e7 100755 --- a/.ci/scripts/check_boilerplate.py +++ b/.ci/scripts/check_boilerplate.py @@ -21,6 +21,7 @@ import sys _EXCLUDE_DIRS = ('.git', '.terraform') +_EXCLUDE_RE = re.compile(r'# skip boilerplate check') _MATCH_FILES = ( 'Dockerfile', '.py', '.sh', '.tf', '.yaml', '.yml' ) @@ -40,8 +41,11 @@ def main(dir): for fname in files: if fname in _MATCH_FILES or os.path.splitext(fname)[1] in _MATCH_FILES: fpath = os.path.abspath(os.path.join(root, fname)) + content = open(fpath).read() + if _EXCLUDE_RE.search(content): + continue try: - if not _MATCH_RE.search(open(fpath).read()): + if not _MATCH_RE.search(content): errors.append(fpath) except (IOError, OSError): warnings.append(fpath) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..abf38dd2b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [Unreleased] + +## [1.0.0] - 2020-03-27 + +- merge development branch with suite of new modules and end-to-end examples + + +[Unreleased]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v0.1...v1.0 diff --git a/README.md b/README.md index 57644e9e1..7ce72a331 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,41 @@ -# Cloud Foundation Toolkit - Fabric +# Terraform Examples and Modules for Google Cloud -Cloud Foundation Fabric provides end-to-end Terraform code examples on GCP, which are meant for prototyping and as minimal samples to aid in designing real-world infrastructures. As such, these samples are meant to be adapted and updated for your different use cases, and often do not implement GCP security best practices for production use. +This repository provides **end-to-end examples** and a **suite of Terraform modules** for Google Cloud, which support different use cases: -All the examples leverage composition, combining different Cloud Foundation Toolkit modules to realize an integrated design. Additional modules can be combined in to tailor the examples to specific needs, and to implement additional best practices. You can check the [full list of Cloud Foundation Toolkit modules here](https://github.com/terraform-google-modules). +- starter kits used to bootstrap real-word cloud foundations and infrastructure +- reference examples used to deep dive on network patterns or product features +- composable modules that support quick prototyping and testing +- a comprehensive source of lean modules that lend themselves well to changes -The examples are organized into two main sections: GCP foundational design, and infrastructure design +The whole repository is meant to be cloned as a single unit, and then forked into separate owned repositories to seed production usage, or used as-is and periodically updated as a complete toolkit for prototyping. -## Foundational examples +Both the examples and modules require some measure of Terraform skills to be used effectively. If you are looking for a feature-rich black box to manage project or product creation with minimal specific skills, you might be better served by the [Cloud Foundation Toolkit](https://registry.terraform.io/modules/terraform-google-modules) suite of modules. -Foundational examples deal with organization-level management of GCP resources, and take care of folder hierarchy, initial automation requirements (service accounts, GCS buckets), and high level best practices like audit log exports and organization policies. +## End-to-end examples -They are simplified versions of real-life use cases, and put a particular emphasis on separation of duties at the environment or tenant level, and decoupling high level permissions from the day to day running of infrastructure automation. More details and the actual examples are available in the [foundations folder](foundations). +The examples in this repository are split in two main sections: **foundational examples** that bootstrap the organizational hierarchy and automation prerequisites, and **infrastructure scenarios** that implement core networking patterns or features. -## Infrastructure examples +Currently available examples: -Infrastructure examples showcase typical networking configurations on GCP, and are meant to illustrate how to automate them with Terraform, and to offer an easy way of testing different scenarios. Like the foundational examples, they are simplified versions of real-life use cases. More details and the actual examples are available in the [infrastructure folder](infrastructure). +- **foundations** - [single level hierarchy](./foundations/environments/) (environments), [multiple level hierarchy](./foundations/business-units/) (business units + environments) +- **infrastructure** - [hub and spoke via peering](./infrastructure/hub-and-spoke-peering/), [hub and spoke via VPN](./infrastructure/hub-and-spoke-vpn/), [DNS and Google Private Access for on-premises](./infrastructure/onprem-google-access-dns/), [Shared VPC with GKE support](./infrastructure/shared-vpc-gke/) + +For more information see the README files in the [foundations](./foundations/) and [infrastructure](./infrastructure/) folders. + +## Modules + +The suite of modules in this repository are designed for rapid composition and reuse, and to be reasonably simple and readable so that they can be forked and changed where use of third party code and sources is not allowed. + +All modules share a similar interface where each module tries to stay close to the underlying provider resources, support IAM together with resource creation and modification, offer the option of creating multiple resources where it makes sense (eg not for projects), and be completely free of side-effects (eg no external commands). + +The current list of modules supports most of the core foundational and networking components used to design end-to-end infrastructure, with more modules in active development for specialized compute, security, and data scenarios. + +Currently available modules: + +- **foundational** - [folders](./modules/folders), [log sinks](./modules/logging-sinks), [project](./modules/project), [service accounts](./modules/iam-service-accounts) +- **networking** - [VPC](./modules/net-vpc), [VPC firewall](./modules/net-vpc-firewall), [VPC peering](./modules/net-vpc-peering), VPN ([static](./modules/net-vpn-static), [dynamic](./modules/net-vpn-dynamic), [HA](./modules/net-vpn-ha)), [NAT](./modules/net-cloudnat), [address reservation](./modules/net-address), [DNS](./modules/dns) +- **compute** - [VM/VM group](./modules/compute-vm), [GKE cluster](./modules/gke-cluster), [GKE nodepool](./modules/gke-nodepool), [COS container](./modules/compute-vm-cos-coredns) +- **data** - [GCS](./modules/gcs), [BigQuery dataset](./modules/bigquery) +- **other** - [KMS](./modules/kms), [on-premises in Docker](./modules/on-prem-in-a-box) + +For more information and usage examples see each module's README file. diff --git a/docker-images/strongswan/Dockerfile b/docker-images/strongswan/Dockerfile new file mode 100644 index 000000000..a804c9203 --- /dev/null +++ b/docker-images/strongswan/Dockerfile @@ -0,0 +1,34 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.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. + +FROM alpine:latest + +RUN set -xe \ + && apk add --no-cache strongswan bash sudo + +COPY entrypoint.sh /entrypoint.sh +RUN chmod 0755 /entrypoint.sh + +COPY ipsec-vti.sh /var/lib/strongswan/ipsec-vti.sh +RUN chmod 0755 /var/lib/strongswan/ipsec-vti.sh + +RUN echo 'ipsec ALL=NOPASSWD:SETENV:/usr/sbin/ipsec,/sbin/ip,/sbin/sysctl' > /etc/sudoers.d/ipsec +RUN chmod 0440 /etc/sudoers.d/ipsec + +ENV VPN_DEVICE=eth0 +ENV LAN_NETWORKS=192.168.0.0/24 + +EXPOSE 500/udp 4500/udp + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker-images/strongswan/README.md b/docker-images/strongswan/README.md new file mode 100644 index 000000000..42225416b --- /dev/null +++ b/docker-images/strongswan/README.md @@ -0,0 +1,44 @@ + +# StrongSwan docker container + +### [strongSwan](https://www.strongswan.org/) is an OpenSource IPsec-based VPN Solution + +### Docker compose example +```yaml +version: "3" +services: + vpn: + image: gcr.io/pso-cft-fabric/strongswan:latest + networks: + default: + ipv4_address: 192.168.0.2 + cap_add: + - NET_ADMIN + ports: + - "500:500/udp" + - "4500:4500/udp" + - "179:179/tcp" + privileged: true + volumes: + - "/lib/modules:/lib/modules:ro" + - "/etc/localtime:/etc/localtime:ro" + - "/var/lib/docker-compose/onprem/ipsec/ipsec.conf:/etc/ipsec.conf:ro" + - "/var/lib/docker-compose/onprem/ipsec/ipsec.secrets:/etc/ipsec.secrets:ro" + - "/var/lib/docker-compose/onprem/ipsec/vti.conf:/etc/strongswan.d/vti.conf:ro" + bird: + image: pierky/bird + network_mode: service:vpn + cap_add: + - NET_ADMIN + - NET_BROADCAST + - NET_RAW + privileged: true + volumes: + - "/var/lib/docker-compose/onprem/bird/bird.conf:/etc/bird/bird.conf:ro" + +``` + +### Build +```bash +gcloud builds submit . --config=cloudbuild.yaml +``` diff --git a/tests/infrastructure/shared_vpc/conftest.py b/docker-images/strongswan/cloudbuild.yaml similarity index 53% rename from tests/infrastructure/shared_vpc/conftest.py rename to docker-images/strongswan/cloudbuild.yaml index cb648e15a..793f9497c 100644 --- a/tests/infrastructure/shared_vpc/conftest.py +++ b/docker-images/strongswan/cloudbuild.yaml @@ -1,10 +1,11 @@ -# Copyright 2019 Google LLC + +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.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 +# 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, @@ -12,16 +13,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -"Plan fixture." +# In this directory, run the following command to build this builder. +# $ gcloud builds submit . --config=cloudbuild.yaml -import os - -import pytest - - -_TFDIR = os.path.sep.join(os.path.abspath(__file__).split(os.path.sep)[-3:-1]) - - -@pytest.fixture(scope='package') -def plan(plan): - return plan(_TFDIR) +steps: +- name: 'gcr.io/cloud-builders/docker' + args: + - build + - --tag=gcr.io/$PROJECT_ID/strongswan + - --tag=gcr.io/$PROJECT_ID/strongswan:latest + - . + +images: + - 'gcr.io/$PROJECT_ID/strongswan:latest' + +timeout: 1200s diff --git a/docker-images/strongswan/entrypoint.sh b/docker-images/strongswan/entrypoint.sh new file mode 100644 index 000000000..afda6f36a --- /dev/null +++ b/docker-images/strongswan/entrypoint.sh @@ -0,0 +1,35 @@ +#!/bin/sh -e + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.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. + +# Enable IP forwarding +sysctl -w net.ipv4.ip_forward=1 + +# Stop ipsec when terminating +_stop_ipsec() { + echo "Shutting down strongSwan/ipsec..." + ipsec stop +} +trap _stop_ipsec SIGTERM + +# Making the containter to work as a default gateway for LAN_NETWORKS +iptables -t nat -A POSTROUTING -s ${LAN_NETWORKS} -o ${VPN_DEVICE} -m policy --dir out --pol ipsec -j ACCEPT +iptables -t nat -A POSTROUTING -s ${LAN_NETWORKS} -o ${VPN_DEVICE} -j MASQUERADE + +# Start ipsec +echo "Starting up strongSwan/ipsec..." +ipsec start --nofork "$@" & +child=$! +wait "$child" diff --git a/docker-images/strongswan/ipsec-vti.sh b/docker-images/strongswan/ipsec-vti.sh new file mode 100644 index 000000000..e686d0c1a --- /dev/null +++ b/docker-images/strongswan/ipsec-vti.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.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. + +# originally published at +# https://cloud.google.com/community/tutorials/using-cloud-vpn-with-strongswan + +set -o nounset +set -o errexit + +IP=$(which ip) + +PLUTO_MARK_OUT_ARR=(${PLUTO_MARK_OUT//// }) +PLUTO_MARK_IN_ARR=(${PLUTO_MARK_IN//// }) + +VTI_TUNNEL_ID=${1} +VTI_REMOTE=${2} +VTI_LOCAL=${3} + +LOCAL_IF="${PLUTO_INTERFACE}" +VTI_IF="vti${VTI_TUNNEL_ID}" +# GCP's MTU is 1460 +GCP_MTU="1460" +# ipsec overhead is 73 bytes, we need to compute new mtu. +VTI_MTU=$((GCP_MTU-73)) + +case "${PLUTO_VERB}" in + up-client) + sudo ${IP} link add ${VTI_IF} type vti local ${PLUTO_ME} remote ${PLUTO_PEER} okey ${PLUTO_MARK_OUT_ARR[0]} ikey ${PLUTO_MARK_IN_ARR[0]} + sudo ${IP} addr add ${VTI_LOCAL} remote ${VTI_REMOTE} dev "${VTI_IF}" + sudo ${IP} link set ${VTI_IF} up mtu ${VTI_MTU} + + # Disable IPSEC Policy + sudo /sbin/sysctl -w net.ipv4.conf.${VTI_IF}.disable_policy=1 + + # Enable loosy source validation, if possible. Otherwise disable validation. + sudo /sbin/sysctl -w net.ipv4.conf.${VTI_IF}.rp_filter=2 || sysctl -w net.ipv4.conf.${VTI_IF}.rp_filter=0 + + # If you would like to use VTI for policy-based you shoud take care of routing by yourselv, e.x. + if [[ "${PLUTO_PEER_CLIENT}" != "0.0.0.0/0" ]]; then + ${IP} r add "${PLUTO_PEER_CLIENT}" dev "${VTI_IF}" + fi + ;; + down-client) + sudo ${IP} tunnel del "${VTI_IF}" + ;; +esac + +# Enable IPv4 forwarding +sudo /sbin/sysctl -w net.ipv4.ip_forward=1 + +# Disable IPSEC Encryption on local net +sudo /sbin/sysctl -w net.ipv4.conf.${LOCAL_IF}.disable_xfrm=1 +sudo /sbin/sysctl -w net.ipv4.conf.${LOCAL_IF}.disable_policy=1 diff --git a/docker-images/toolbox/Dockerfile b/docker-images/toolbox/Dockerfile new file mode 100644 index 000000000..31bc1a6cb --- /dev/null +++ b/docker-images/toolbox/Dockerfile @@ -0,0 +1,30 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.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. + + +FROM google/cloud-sdk:alpine + +COPY entrypoint.sh /entrypoint.sh +RUN chmod 0755 /entrypoint.sh + +RUN apk update && \ + apk add bash curl bind-tools busybox-extras netcat-openbsd && \ + rm /var/cache/apk/* + +RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl && \ + chmod 755 kubectl && mv kubectl /usr/local/bin/ + +CMD ["/bin/bash"] + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker-images/toolbox/README.md b/docker-images/toolbox/README.md new file mode 100644 index 000000000..6e2f70c24 --- /dev/null +++ b/docker-images/toolbox/README.md @@ -0,0 +1,26 @@ + +# ToolBox docker container + +Lightweight container with some basic console tools used for testing and probing. + +## Building + +```bash +gcloud builds submit . --config=cloudbuild.yaml +``` + +## Docker compose + +```yaml +version: "3" +services: + vpn: + image: gcr.io/pso-cft-fabric/toolbox:latest + networks: + default: + ipv4_address: 192.168.0.5 + cap_add: + - NET_ADMIN + privileged: true + +``` diff --git a/tests/foundations/business_units/conftest.py b/docker-images/toolbox/cloudbuild.yaml similarity index 53% rename from tests/foundations/business_units/conftest.py rename to docker-images/toolbox/cloudbuild.yaml index cb648e15a..6d123129a 100644 --- a/tests/foundations/business_units/conftest.py +++ b/docker-images/toolbox/cloudbuild.yaml @@ -1,10 +1,11 @@ -# Copyright 2019 Google LLC + +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.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 +# 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, @@ -12,16 +13,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -"Plan fixture." +# In this directory, run the following command to build this builder. +# $ gcloud builds submit . --config=cloudbuild.yaml -import os - -import pytest - - -_TFDIR = os.path.sep.join(os.path.abspath(__file__).split(os.path.sep)[-3:-1]) - - -@pytest.fixture(scope='package') -def plan(plan): - return plan(_TFDIR) +steps: +- name: 'gcr.io/cloud-builders/docker' + args: + - build + - --tag=gcr.io/$PROJECT_ID/toolbox + - --tag=gcr.io/$PROJECT_ID/toolbox:latest + - . + +images: + - 'gcr.io/$PROJECT_ID/toolbox:latest' + +timeout: 1200s diff --git a/docker-images/toolbox/entrypoint.sh b/docker-images/toolbox/entrypoint.sh new file mode 100644 index 000000000..31f37caea --- /dev/null +++ b/docker-images/toolbox/entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/sh -e + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.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. + +echo "Entering sleep..." +trap : TERM INT; (while true; do sleep 1000; done) & wait diff --git a/foundations/README.md b/foundations/README.md index b8799d9c8..097cd486a 100644 --- a/foundations/README.md +++ b/foundations/README.md @@ -1,40 +1,48 @@ -# Organization-level bootstrap samples +# Cloud foundation examples -This set of Terraform root modules is designed with two main purposes in mind: automating the organizational layout, and bootstrapping the initial resources and the corresponding IAM roles, which will then be used to automate the actual infrastructure. +The examples in this folder deal with cloud foundations: the set of resources used to **create the organizational hierarchy** (folders and specific IAM roles), **implement top-level initial best practices** (audit log exports, policies) and **bootstrap infrastructure automation** (GCS buckets, service accounts and IAM roles). -Despite being fairly generic, these modules closely match some of the initial automation stages we have implemented in actual customer engagements, and are purposely kept simple to offer a good starting point for further customizations. +The examples are derived from actual production use cases, and are meant to be used as-is, or extended to create more complex hierarchies. The guiding principles they implement are: -There are several advantages in using an initial stage like the ones provided here: +- divide the hierarchy in separate partitions along environment/organization boundaries, to enforce separation of duties and decouple organization admin permissions from the day-to-day running of infrastructure +- keep top-level Terraform code minimal and encapsulate complexity in modules, to ensure readability and allow using code as high level documentation -- automate and parameterize creation of the organizational layout, documenting it through code -- use a single declarative tool to create and manage both prerequisites and the actual infrastructure, eliminating the need for manual commands, scripts or external tools -- enforce separation of duties at the environment (or tenant in multi-tenant architectures) level, by automating creation of per-environment Terraform service accounts, their IAM roles, and GCS buckets to hold state -- decouple and document the use of organization-level permissions from the day to day management of the actual infrastructure, by assigning a minimum but sufficient set of high level IAM roles to Terraform service accounts in an initial stage -- provide a sane place for the creation and management of shared resources that are not tied to a specific environment +## Examples + +### Environment Hierarchy + + This [example](./environments/) implements a simple one-level oganizational layout, which is commonly used to bootstrap small infrastructures, or in situations where lower level folders are managed with separate, more granular Terraform setups. + +One authoritative service account, one bucket and one folder are created for each environment, together with top-level shared resources. This example's simplicity makes it a good starting point to understand and prototype foundational design. + +
+ +### Business Unit / Environment Hierarchy + + This [example](./business-units/) implements a two-level organizational layout, with a first level usually mapped to business units, and a second level implementing identical environments (prod, test, etc.) under each first-level folder. + +This approach maps well to medium sized infrastructures, and can be used as a starting point for more complex scenarios. Separate Terraform stages are then usually implemented for each business unit, implementing fine-grained project and service account creation for individual application teams. +
## Operational considerations -This specific type of preliminary automation stage is usually fairly static, only changing when a new environment or shared resource is added or modified, and lends itself well to being applied manually by organization administrators. One secondary advantage of running this initial stage manually, is eliminating the need to create and manage automation credentials that embed sensitive permissions scoped at the organization or root folder level. +These examples are always used manually, as they require very high-level permissions and are updated infrequently. -### IAM roles +The IAM roles needed are: -This type of automation stage needs very specific IAM roles on the root node (organization or folder), and additional roles at the organization level if the generated service accounts for automation need to be able to create and manage Shared VPC. The needed roles are: +- Project Creator, Folder Administrator, Logging Administrator on the root node (org or folder) +- Billing Account Administrator on the billing account or org +- Organization Administrator if Shared VPC roles have to be granted to the automation service accounts created for each scope -- on the root node Project Creator, Folder Administrator, Logging Administrator -- on the billing account or organization Billing Account Administrator -- on the organization Organization Administrator, if Shared VPC needs to be managed by the automation service accounts +State is local on the first run, then it should be moved to the GCS bucket created by the examples for this specific purpose: -### State - -This type of stage creates the prerequisites for Terraform automation including the GCS bucket used for its own remote state, so some care needs to be used when running it for the first time, when its GCS bucket has not yet been created. - -After the first successful `terraform apply`, copy the `backend.tf.sample` file -to `backend.tf`, then set the bucket name to the one shown in the `bootstrap_tf_gcs_bucket` output in the new file. Once that is done, run `terraform apply` again to transfer local state to the remote GCS bucket. From then on, state will be remote. - -### Things to be aware of - -Using `count` in Terraform resources has the [well-known limitation](https://github.com/hashicorp/terraform/issues/18767) that changing the variable controlling `count` results in the potential unwanted deletion of resources. - -These samples use `count` on the `environments` list variable to manage multiples for key resources like service accounts, GCS buckets, and IAM roles. Environment names are usually stable, but care must still be taken in defining the initial list so that names are final, and names for temporary environments (if any are needed) are last so they don't trigger recreation of resources based on the following elements in the list. - -This issue will be addressed in a future release of these examples, by replacing `count` with the new `foreach` construct [introduced in Terraform 0.12.6](https://twitter.com/mitchellh/status/1156661893789966336?lang=en) that uses key-based indexing. +```bash +# first apply +terraform apply +# create backend file +cp backend.tf.sample backend.tf +# edit backend.tf and use bootstrap_tf_gcs_bucket output for GCS bucket name +vi backend.tf +# once done, move local state to GCS bucket +terraform init +``` diff --git a/foundations/business-units/README.md b/foundations/business-units/README.md index 684579bc0..7c7404589 100644 --- a/foundations/business-units/README.md +++ b/foundations/business-units/README.md @@ -2,7 +2,7 @@ This sample creates an organizational layout with two folder levels, where the first level is usually mapped to one business unit or team (infra, data, analytics) and the second level represents enviroments (prod, test). It also sets up all prerequisites for automation (GCS state buckets, service accounts, etc.), and the correct roles on those to enforce separation of duties at the environment level. -This layout is well suited for medium-sized infrastructures managed by different sets of teams, and especially where the foundational infrastructure needs to be managed centrally, as the top-level automation service accounts for each environment allow cross-team management of the base resources (projects, IAM, etc.). +This layout is well suited for medium-sized infrastructures managed by different sets of teams, and in cases where the core infrastructure is managed centrally, as the top-level automation service accounts for each environment allow cross-team management of the base resources (projects, IAM, etc.). ![High-level diagram](diagram.png "High-level diagram") @@ -19,7 +19,7 @@ This sample creates several distinct groups of resources: - one project in the shared folder to set up and host centralized audit log exports - one project in the shared folder to hold services used across environments like GCS, GCR, KMS, Cloud Build, etc. -The number of resources in this sample is kept to a minimum so as to make it generally applicable, more resources can be easily added by leveraging the full array of [Cloud Foundation Toolkit modules](https://github.com/terraform-google-modules), especially in the shared services project. +The number of resources in this sample is kept to a minimum so as to make it generally applicable, more resources can be easily added by leveraging other [modules from our bundle](../../modules/), or from other sources like the [CFT suite](https://github.com/terraform-google-modules). ## Shared services @@ -31,38 +31,28 @@ This sample uses a top-level folder to encapsulate projects that host resources | name | description | type | required | default | |---|---|:---: |:---:|:---:| | billing_account_id | Billing account id used as default for new projects. | string | ✓ | | -| business_unit_1_name | Business unit 1 short name. | string | ✓ | | -| business_unit_2_name | Business unit 2 short name. | string | ✓ | | -| business_unit_3_name | Business unit 3 short name. | string | ✓ | | -| environments | Environment short names. | list(string) | ✓ | | | organization_id | Organization id. | string | ✓ | | | prefix | Prefix used for resources that need unique names. | string | ✓ | | | root_node | Root node for the new hierarchy, either 'organizations/org_id' or 'folders/folder_id'. | string | ✓ | | -| *audit_viewers* | Audit project viewers, in IAM format. | list(string) | | [] | -| *gcs_location* | GCS bucket location. | string | | EU | -| *generate_service_account_keys* | Generate and store service account keys in the state file. | bool | | false | +| *audit_filter* | Audit log filter used for the log sink. | string | | ... | +| *environments* | Environment short names. | map(string) | | ... | +| *gcs_defaults* | Defaults use for the state GCS buckets. | map(string) | | ... | +| *iam_audit_viewers* | Audit project viewers, in IAM format. | list(string) | | [] | +| *iam_shared_owners* | Shared services project owners, in IAM format. | list(string) | | [] | +| *iam_terraform_owners* | Terraform project owners, in IAM format. | list(string) | | [] | | *project_services* | Service APIs enabled by default in new projects. | list(string) | | ... | -| *shared_bindings_members* | List of comma-delimited IAM-format members for the additional shared project bindings. | list(string) | | [] | -| *shared_bindings_roles* | List of roles for additional shared project bindings. | list(string) | | [] | -| *terraform_owners* | Terraform project owners, in IAM format. | list(string) | | [] | ## Outputs | name | description | sensitive | |---|---|:---:| -| audit_logs_bq_dataset | Bigquery dataset for the audit logs export. | | | audit_logs_project | Project that holds the audit logs export resources. | | | bootstrap_tf_gcs_bucket | GCS bucket used for the bootstrap Terraform state. | | -| business_unit_1_environment_folders_ids | Business unit 1 environment folders. | | -| business_unit_1_folder_id | Business unit 1 top-level folder ID. | | -| business_unit_2_environment_folders_ids | Business unit 2 environment folders. | | -| business_unit_2_folder_id | Business unit 2 top-level folder ID. | | -| business_unit_3_environment_folders_ids | Business unit 3 environment folders. | | -| business_unit_3_folder_id | Business unit 3 top-level folder ID. | | -| environment_service_account_keys | Service account keys used to run each environment Terraform modules. | ✓ | -| environment_service_accounts | Service accounts used to run each environment Terraform modules. | | -| environment_tf_gcs_buckets | GCS buckets used for each environment Terraform state. | | -| shared_folder_id | Shared folder ID. | | +| bu_bi | Business Intelligence attributes. | | +| bu_bi_sa_keys | Business Intelligence service account keys. | ✓ | +| bu_ml | Machine Learning attributes. | | +| bu_ml_sa_keys | Machine Learning service account keys. | ✓ | +| shared_folder_id | Shared folder id. | | | shared_resources_project | Project that holdes resources shared across business units. | | | terraform_project | Project that holds the base Terraform resources. | | diff --git a/foundations/business-units/backend.tf.sample b/foundations/business-units/backend.tf.sample index 19ccfaf66..d63060c66 100644 --- a/foundations/business-units/backend.tf.sample +++ b/foundations/business-units/backend.tf.sample @@ -1,4 +1,4 @@ -# Copyright 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,4 +19,4 @@ terraform { # run apply again to transfer state bucket = "" } -} \ No newline at end of file +} diff --git a/foundations/business-units/main.tf b/foundations/business-units/main.tf index 212312617..76b01d144 100644 --- a/foundations/business-units/main.tf +++ b/foundations/business-units/main.tf @@ -1,130 +1,86 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -# TODO(averbukh): simplify log-sink parameters once https://github.com/terraform-google-modules/terraform-google-log-export/issues/28 is done. - -locals { - parent_numeric_id = element(split("/", var.root_node), 1) - log_sink_parent_resource_type = element(split("/", var.root_node), 0) == "organizations" ? "organization" : "folder" - log_sink_name = element(split("/", var.root_node), 0) == "organizations" ? "logs-audit-org-${local.parent_numeric_id}" : "logs-audit-folder-${local.parent_numeric_id}" -} - -############################################################################### -# Shared resources folder # -############################################################################### - -module "shared-folder" { - source = "terraform-google-modules/folders/google" - version = "2.0.2" - parent = var.root_node - names = ["shared"] -} +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 top-level resources # ############################################################################### -# Terraform project +# Shared folder -module "project-tf" { - source = "terraform-google-modules/project-factory/google//modules/fabric-project" - version = "5.0.0" - parent = module.shared-folder.id - billing_account = var.billing_account_id - prefix = var.prefix - name = "terraform" - lien_reason = "terraform" - owners = var.terraform_owners - activate_apis = var.project_services +module "shared-folder" { + source = "../../modules/folders" + parent = var.root_node + names = ["shared"] } -# Per environment service accounts +# Terraform project -module "service-accounts-tf-environments" { - source = "terraform-google-modules/service-accounts/google" - version = "2.0.2" - project_id = module.project-tf.project_id - org_id = var.organization_id - billing_account_id = var.billing_account_id - prefix = var.prefix - names = var.environments - grant_billing_role = true - generate_keys = var.generate_service_account_keys +module "tf-project" { + source = "../../modules/project" + name = "terraform" + parent = module.shared-folder.id + prefix = var.prefix + billing_account = var.billing_account_id + iam_additive_members = { "roles/owner" = var.iam_terraform_owners } + iam_additive_roles = ["roles/owner"] + services = var.project_services } # Bootstrap Terraform state GCS bucket -module "gcs-tf-bootstrap" { - source = "terraform-google-modules/cloud-storage/google" - version = "1.0.0" - project_id = module.project-tf.project_id - prefix = "${var.prefix}-tf" +module "tf-gcs-bootstrap" { + source = "../../modules/gcs" + project_id = module.tf-project.project_id names = ["tf-bootstrap"] - location = var.gcs_location -} - -# Per environment Terraform state GCS buckets - -module "gcs-tf-environments" { - source = "terraform-google-modules/cloud-storage/google" - version = "1.0.0" - project_id = module.project-tf.project_id - prefix = "${var.prefix}-tf" - names = var.environments - location = var.gcs_location - set_admin_roles = true - bucket_admins = zipmap( - var.environments, - module.service-accounts-tf-environments.iam_emails_list - ) + prefix = "${var.prefix}-tf" + location = var.gcs_defaults.location } ############################################################################### -# Business units # +# Business units # ############################################################################### -# Business unit 1 - -module "business-unit-1-folders" { - source = "./modules/business-unit-folders" - business_unit_folder_name = var.business_unit_1_name - environments = var.environments - per_folder_admins = module.service-accounts-tf-environments.iam_emails_list - root_node = var.root_node - +module "bu-business-intelligence" { + source = "../../modules/folders-unit" + name = "Business Intelligence" + short_name = "bi" + automation_project_id = module.tf-project.project_id + billing_account_id = var.billing_account_id + environments = var.environments + gcs_defaults = var.gcs_defaults + organization_id = var.organization_id + root_node = var.root_node + # extra variables from the folders-unit module can be used here to grant + # IAM roles to the bu users, configure the automation service accounts, etc. + # iam_roles = ["viewer"] + # iam_members = { viewer = ["user:user@example.com"] } } -# Business unit 2 - -module "business-unit-2-folders" { - source = "./modules/business-unit-folders" - business_unit_folder_name = var.business_unit_2_name - environments = var.environments - per_folder_admins = module.service-accounts-tf-environments.iam_emails_list - root_node = var.root_node - -} - -# Business unit 3 - -module "business-unit-3-folders" { - source = "./modules/business-unit-folders" - business_unit_folder_name = var.business_unit_3_name - environments = var.environments - per_folder_admins = module.service-accounts-tf-environments.iam_emails_list - root_node = var.root_node - +module "bu-machine-learning" { + source = "../../modules/folders-unit" + name = "Machine Learning" + short_name = "ml" + automation_project_id = module.tf-project.project_id + billing_account_id = var.billing_account_id + environments = var.environments + gcs_defaults = var.gcs_defaults + organization_id = var.organization_id + root_node = var.root_node + # extra variables from the folders-unit module can be used here to grant + # IAM roles to the bu users, configure the automation service accounts, etc. } ############################################################################### @@ -133,42 +89,50 @@ module "business-unit-3-folders" { # Audit logs project -module "project-audit" { - source = "terraform-google-modules/project-factory/google//modules/fabric-project" - version = "5.0.0" - parent = module.shared-folder.id - billing_account = var.billing_account_id - prefix = var.prefix +module "audit-project" { + source = "../../modules/project" name = "audit" - lien_reason = "audit" - viewers = var.audit_viewers - activate_apis = concat(var.project_services, [ + parent = var.root_node + prefix = var.prefix + billing_account = var.billing_account_id + iam_members = { + "roles/bigquery.dataEditor" = [module.audit-log-sinks.writer_identities[0]] + "roles/viewer" = var.iam_audit_viewers + } + iam_roles = [ + "roles/bigquery.dataEditor", + "roles/viewer" + ] + services = concat(var.project_services, [ "bigquery.googleapis.com", ]) } -# Audit logs destination on BigQuery +# audit logs dataset and sink -module "bq-audit-export" { - source = "terraform-google-modules/log-export/google//modules/bigquery" - version = "3.2.0" - project_id = module.project-audit.project_id - dataset_name = "${replace(local.log_sink_name, "-", "_")}" - log_sink_writer_identity = module.log-sink-audit.writer_identity +module "audit-datasets" { + source = "../../modules/bigquery" + project_id = module.audit-project.project_id + datasets = { + audit_export = { + name = "Audit logs export." + description = "Terraform managed." + location = "EU" + labels = null + options = null + } + } } -# Audit log sink for root node - -module "log-sink-audit" { - source = "terraform-google-modules/log-export/google" - version = "3.2.0" - filter = "logName: \"/logs/cloudaudit.googleapis.com%2Factivity\" OR logName: \"/logs/cloudaudit.googleapis.com%2Fsystem_event\"" - log_sink_name = local.log_sink_name - parent_resource_type = local.log_sink_parent_resource_type - parent_resource_id = local.parent_numeric_id - include_children = "true" - unique_writer_identity = "true" - destination_uri = "${module.bq-audit-export.destination_uri}" +module "audit-log-sinks" { + source = "../../modules/logging-sinks" + parent = var.root_node + destinations = { + audit-logs = "bigquery.googleapis.com/projects/${module.audit-project.project_id}/datasets/${try(module.audit-datasets.names[0], "")}" + } + sinks = { + audit-logs = var.audit_filter + } } ############################################################################### @@ -177,17 +141,19 @@ module "log-sink-audit" { # Shared resources project -module "project-shared-resources" { - source = "terraform-google-modules/project-factory/google//modules/fabric-project" - version = "5.0.0" - parent = module.shared-folder.id - billing_account = var.billing_account_id - prefix = var.prefix - name = "shared" - lien_reason = "shared" - activate_apis = var.project_services - extra_bindings_roles = var.shared_bindings_roles - extra_bindings_members = var.shared_bindings_members +module "shared-project" { + source = "../../modules/project" + name = "shared" + parent = module.shared-folder.id + prefix = var.prefix + billing_account = var.billing_account_id + iam_additive_members = { + "roles/owner" = var.iam_shared_owners + } + iam_additive_roles = [ + "roles/owner" + ] + services = var.project_services } # Add further modules here for resources that are common to all business units diff --git a/foundations/business-units/modules/business-unit-folders/README.md b/foundations/business-units/modules/business-unit-folders/README.md deleted file mode 100644 index 89081edb4..000000000 --- a/foundations/business-units/modules/business-unit-folders/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Two-level folders tree - -This module is a simple wrapper for the [Cloud Foundation Folder module](https://github.com/terraform-google-modules/terraform-google-folders), that manages one folder and one child folder under it, for each name passed in the `environments` variable. It is meant to be used for organizational layouts where a predefined number of folders representing environments, are created as child folders for business units or teams. - -For details on how the IAM variables work, please refer to the [Cloud Foundation Folder module](https://github.com/terraform-google-modules/terraform-google-folders). - - -## Inputs - -| Name | Description | Type | Default | Required | -|------|-------------|:----:|:-----:|:-----:| -| business\_unit\_folder\_name | Business Unit Folder name. | string | n/a | yes | -| environments | Environment short names. | list(string) | n/a | yes | -| per\_folder\_admins | List of IAM-style members per folder who will get extended permissions. | list(string) | `` | no | -| root\_node | Root node for the new hierarchy, either 'organizations/org_id' or 'folders/folder_id'. | string | n/a | yes | - -## Outputs - -| Name | Description | -|------|-------------| -| business\_unit\_folder\_id | Business Unit Folder ID. | -| environment\_folders\_ids | Environment folders IDs. | - - diff --git a/foundations/business-units/modules/business-unit-folders/main.tf b/foundations/business-units/modules/business-unit-folders/main.tf deleted file mode 100644 index 0b48b5444..000000000 --- a/foundations/business-units/modules/business-unit-folders/main.tf +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -############################################################################### -# Business Unit Folder # -############################################################################### - -module "business-unit-folder" { - source = "terraform-google-modules/folders/google" - version = "2.0.2" - parent = var.root_node - names = [var.business_unit_folder_name] -} - -############################################################################### -# Environment Folders # -############################################################################### - -module "environment-folders" { - source = "terraform-google-modules/folders/google" - version = "2.0.2" - parent = module.business-unit-folder.id - names = var.environments - set_roles = true - per_folder_admins = var.per_folder_admins - folder_admin_roles = [ - "roles/resourcemanager.folderViewer", - "roles/resourcemanager.projectCreator", - "roles/owner", - "roles/compute.networkAdmin", - "roles/compute.xpnAdmin" - ] -} diff --git a/foundations/business-units/modules/business-unit-folders/outputs.tf b/foundations/business-units/modules/business-unit-folders/outputs.tf deleted file mode 100644 index 78e4b5d85..000000000 --- a/foundations/business-units/modules/business-unit-folders/outputs.tf +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -output "business_unit_folder_id" { - description = "Business Unit Folder ID." - value = module.business-unit-folder.id -} - -output "environment_folders_ids" { - description = "Environment folders IDs." - value = module.environment-folders.ids -} - -# Add further outputs here for the additional modules that manage shared -# resources, like GCR, GCS buckets, KMS, etc. diff --git a/foundations/business-units/modules/business-unit-folders/variables.tf b/foundations/business-units/modules/business-unit-folders/variables.tf deleted file mode 100644 index 936eda687..000000000 --- a/foundations/business-units/modules/business-unit-folders/variables.tf +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -variable "business_unit_folder_name" { - description = "Business Unit Folder name." - type = string -} - -variable "environments" { - description = "Environment short names." - type = list(string) -} - -variable "per_folder_admins" { - type = list(string) - description = "List of IAM-style members per folder who will get extended permissions." - default = [] -} - -variable "root_node" { - description = "Root node for the new hierarchy, either 'organizations/org_id' or 'folders/folder_id'." - type = string -} diff --git a/foundations/business-units/outputs.tf b/foundations/business-units/outputs.tf index b82a102aa..51d5c2134 100644 --- a/foundations/business-units/outputs.tf +++ b/foundations/business-units/outputs.tf @@ -1,91 +1,75 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "terraform_project" { description = "Project that holds the base Terraform resources." - value = module.project-tf.project_id + value = module.tf-project.project_id } output "bootstrap_tf_gcs_bucket" { description = "GCS bucket used for the bootstrap Terraform state." - value = module.gcs-tf-bootstrap.name -} - -output "environment_tf_gcs_buckets" { - description = "GCS buckets used for each environment Terraform state." - value = module.gcs-tf-environments.names -} - -output "environment_service_account_keys" { - description = "Service account keys used to run each environment Terraform modules." - sensitive = true - value = module.service-accounts-tf-environments.keys -} - -output "environment_service_accounts" { - description = "Service accounts used to run each environment Terraform modules." - value = module.service-accounts-tf-environments.emails + value = module.tf-gcs-bootstrap.name } output "shared_folder_id" { - description = "Shared folder ID." + description = "Shared folder id." value = module.shared-folder.id } -output "business_unit_1_folder_id" { - description = "Business unit 1 top-level folder ID." - value = module.business-unit-1-folders.business_unit_folder_id + +output "bu_machine_learning" { + description = "Machine Learning attributes." + value = { + unit_folder = module.bu-machine-learning.unit_folder, + env_gcs_buckets = module.bu-machine-learning.env_gcs_buckets + env_folders = module.bu-machine-learning.env_folders + env_service_accounts = module.bu-machine-learning.env_service_accounts + } } -output "business_unit_1_environment_folders_ids" { - description = "Business unit 1 environment folders." - value = module.business-unit-1-folders.environment_folders_ids +output "bu_machine_learning_keys" { + description = "Machine Learning service account keys." + sensitive = true + value = module.bu-machine-learning.env_sa_keys } -output "business_unit_2_folder_id" { - description = "Business unit 2 top-level folder ID." - value = module.business-unit-2-folders.business_unit_folder_id +output "bu_business_intelligence" { + description = "Business Intelligence attributes." + value = { + unit_folder = module.bu-business-intelligence.unit_folder, + env_gcs_buckets = module.bu-business-intelligence.env_gcs_buckets + env_folders = module.bu-business-intelligence.env_folders + env_service_accounts = module.bu-business-intelligence.env_service_accounts + } } -output "business_unit_2_environment_folders_ids" { - description = "Business unit 2 environment folders." - value = module.business-unit-2-folders.environment_folders_ids -} - -output "business_unit_3_folder_id" { - description = "Business unit 3 top-level folder ID." - value = module.business-unit-3-folders.business_unit_folder_id -} - -output "business_unit_3_environment_folders_ids" { - description = "Business unit 3 environment folders." - value = module.business-unit-3-folders.environment_folders_ids -} - -output "audit_logs_bq_dataset" { - description = "Bigquery dataset for the audit logs export." - value = module.bq-audit-export.resource_name +output "bu_business_intelligence_keys" { + description = "Business Intelligence service account keys." + sensitive = true + value = module.bu-business-intelligence.env_sa_keys } output "audit_logs_project" { description = "Project that holds the audit logs export resources." - value = module.project-audit.project_id + value = module.audit-project.project_id } output "shared_resources_project" { description = "Project that holdes resources shared across business units." - value = module.project-shared-resources.project_id + value = module.shared-project.project_id } # Add further outputs here for the additional modules that manage shared -# resources, like GCR, GCS buckets, KMS, etc. \ No newline at end of file +# resources, like GCR, GCS buckets, KMS, etc. diff --git a/foundations/business-units/providers.tf b/foundations/business-units/providers.tf index b166f75dc..d57e94c5d 100644 --- a/foundations/business-units/providers.tf +++ b/foundations/business-units/providers.tf @@ -1,15 +1,17 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -provider "google" {} \ No newline at end of file +provider "google" {} diff --git a/foundations/business-units/terraform.tfvars.sample b/foundations/business-units/terraform.tfvars.sample new file mode 100644 index 000000000..5545bf3f5 --- /dev/null +++ b/foundations/business-units/terraform.tfvars.sample @@ -0,0 +1,19 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.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. + +billing_account_id = "014617-19UCBC-AF02D9" +organization_id= "500001140800" +prefix = "xyz" +root_node = "folders/9572793983696" +generate_keys = true diff --git a/foundations/business-units/variables.tf b/foundations/business-units/variables.tf index 3dbecffd1..393d76352 100644 --- a/foundations/business-units/variables.tf +++ b/foundations/business-units/variables.tf @@ -1,21 +1,27 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "audit_viewers" { - description = "Audit project viewers, in IAM format." - type = list(string) - default = [] +variable "audit_filter" { + description = "Audit log filter used for the log sink." + type = string + default = <string | ✓ | | +| billing_account_id | Billing account id used as to create projects. | string | ✓ | | | environments | Environment short names. | list(string) | ✓ | | -| organization_id | Organization id. | string | ✓ | | +| organization_id | Organization id in organizations/nnnnnnnn format. | string | ✓ | | | prefix | Prefix used for resources that need unique names. | string | ✓ | | | root_node | Root node for the new hierarchy, either 'organizations/org_id' or 'folders/folder_id'. | string | ✓ | | -| *audit_viewers* | Audit project viewers, in IAM format. | list(string) | | [] | +| *audit_filter* | Audit log filter used for the log sink. | string | | ... | | *gcs_location* | GCS bucket location. | string | | EU | -| *generate_service_account_keys* | Generate and store service account keys in the state file. | bool | | false | -| *grant_xpn_folder_roles* | Grant roles needed for Shared VPC creation to service accounts at the environment folder level. | bool | | true | -| *grant_xpn_org_roles* | Grant roles needed for Shared VPC creation to service accounts at the organization level. | bool | | false | +| *iam_assets_editors* | Shared assets project editors, in IAM format. | list(string) | | [] | +| *iam_assets_owners* | Shared assets project owners, in IAM format. | list(string) | | [] | +| *iam_audit_viewers* | Audit project viewers, in IAM format. | list(string) | | [] | +| *iam_billing_config* | Control granting billing user role to service accounts. Target the billing account by default. | object({...}) | | ... | +| *iam_folder_roles* | List of roles granted to each service account on its respective folder (excluding XPN roles). | list(string) | | ... | +| *iam_sharedsvc_owners* | Shared services project owners, in IAM format. | list(string) | | [] | +| *iam_terraform_owners* | Terraform project owners, in IAM format. | list(string) | | [] | +| *iam_xpn_config* | Control granting Shared VPC creation roles to service accounts. Target the root node by default. | object({...}) | | ... | | *project_services* | Service APIs enabled by default in new projects. | list(string) | | ... | -| *shared_bindings_members* | List of comma-delimited IAM-format members for the additional shared project bindings. | list(string) | | [] | -| *shared_bindings_roles* | List of roles for additional shared project bindings. | list(string) | | [] | -| *terraform_owners* | Terraform project owners, in IAM format. | list(string) | | [] | +| *service_account_keys* | Generate and store service account keys in the state file. | bool | | true | ## Outputs @@ -59,5 +62,4 @@ If no shared services are needed, the shared service project module can of cours | environment_service_accounts | Service accounts used to run each environment Terraform modules. | | | environment_tf_gcs_buckets | GCS buckets used for each environment Terraform state. | | | shared_resources_project | Project that holdes resources shared across environments. | | -| terraform_project | Project that holds the base Terraform resources. | | diff --git a/foundations/environments/locals.tf b/foundations/environments/locals.tf new file mode 100644 index 000000000..4c803cb7e --- /dev/null +++ b/foundations/environments/locals.tf @@ -0,0 +1,39 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + folder_roles = concat(var.iam_folder_roles, local.sa_xpn_folder_role) + organization_id = element(split("/", var.organization_id), 1) + sa_billing_account_role = ( + var.iam_billing_config.target_org ? [] : ["roles/billing.user"] + ) + sa_billing_org_role = ( + ! var.iam_billing_config.target_org ? [] : ["roles/billing.user"] + ) + sa_xpn_folder_role = ( + local.sa_xpn_target_org ? [] : ["roles/compute.xpnAdmin"] + ) + sa_xpn_org_roles = ( + local.sa_xpn_target_org + ? ["roles/compute.xpnAdmin", "roles/resourcemanager.organizationViewer"] + : ["roles/resourcemanager.organizationViewer"] + ) + sa_xpn_target_org = ( + var.iam_xpn_config.target_org + || + substr(var.root_node, 0, 13) == "organizations" + ) +} diff --git a/foundations/environments/main.tf b/foundations/environments/main.tf index 1d8da9229..043e0004e 100644 --- a/foundations/environments/main.tf +++ b/foundations/environments/main.tf @@ -18,80 +18,84 @@ # Terraform project -module "project-tf" { - source = "terraform-google-modules/project-factory/google//modules/fabric-project" - version = "5.0.0" - parent = var.root_node - billing_account = var.billing_account_id - prefix = var.prefix - name = "terraform" - # lien_reason = "terraform" - owners = var.terraform_owners - activate_apis = var.project_services +module "tf-project" { + source = "../../modules/project" + name = "terraform" + parent = var.root_node + prefix = var.prefix + billing_account = var.billing_account_id + iam_additive_members = { "roles/owner" = var.iam_terraform_owners } + iam_additive_roles = ["roles/owner"] + services = var.project_services } # per-environment service accounts -module "service-accounts-tf-environments" { - source = "terraform-google-modules/service-accounts/google" - version = "2.0.2" - project_id = module.project-tf.project_id - org_id = var.organization_id - billing_account_id = var.billing_account_id - prefix = var.prefix - names = var.environments - grant_billing_role = true - grant_xpn_roles = var.grant_xpn_org_roles - generate_keys = var.generate_service_account_keys +module "tf-service-accounts" { + source = "../../modules/iam-service-accounts" + project_id = module.tf-project.project_id + names = var.environments + prefix = var.prefix + iam_billing_roles = { + (var.billing_account_id) = ( + var.iam_billing_config.grant ? local.sa_billing_account_role : [] + ) + } + # folder roles are set in the folders module using authoritative bindings + iam_organization_roles = { + (local.organization_id) = concat( + var.iam_billing_config.grant ? local.sa_billing_org_role : [], + var.iam_xpn_config.grant ? local.sa_xpn_org_roles : [] + ) + } + generate_keys = var.service_account_keys } # bootstrap Terraform state GCS bucket -module "gcs-tf-bootstrap" { - source = "terraform-google-modules/cloud-storage/google" - version = "1.0.0" - project_id = module.project-tf.project_id - prefix = "${var.prefix}-tf" +module "tf-gcs-bootstrap" { + source = "../../modules/gcs" + project_id = module.tf-project.project_id names = ["tf-bootstrap"] + prefix = "${var.prefix}-tf" location = var.gcs_location } # per-environment Terraform state GCS buckets -module "gcs-tf-environments" { - source = "terraform-google-modules/cloud-storage/google" - version = "1.0.0" - project_id = module.project-tf.project_id - prefix = "${var.prefix}-tf" - names = var.environments - location = var.gcs_location - set_admin_roles = true - bucket_admins = zipmap( - var.environments, - module.service-accounts-tf-environments.iam_emails_list - ) +module "tf-gcs-environments" { + source = "../../modules/gcs" + project_id = module.tf-project.project_id + names = var.environments + prefix = "${var.prefix}-tf" + location = var.gcs_location + iam_roles = { + for name in var.environments : (name) => ["roles/storage.objectAdmin"] + } + iam_members = { + for name in var.environments : (name) => { + "roles/storage.objectAdmin" = [module.tf-service-accounts.iam_emails[name]] + } + } } ############################################################################### # Top-level folders # ############################################################################### -module "folders-top-level" { - source = "terraform-google-modules/folders/google" - version = "2.0.2" - parent = var.root_node - names = var.environments - set_roles = true - per_folder_admins = module.service-accounts-tf-environments.iam_emails_list - folder_admin_roles = compact( - [ - "roles/compute.networkAdmin", - "roles/owner", - "roles/resourcemanager.folderViewer", - "roles/resourcemanager.projectCreator", - var.grant_xpn_folder_roles ? "roles/compute.xpnAdmin" : "" - ] - ) +module "environment-folders" { + source = "../../modules/folders" + parent = var.root_node + names = var.environments + iam_roles = { + for name in var.environments : (name) => local.folder_roles + } + iam_members = { + for name in var.environments : (name) => { + for role in local.folder_roles : + (role) => [module.tf-service-accounts.iam_emails[name]] + } + } } ############################################################################### @@ -100,43 +104,50 @@ module "folders-top-level" { # audit logs project -module "project-audit" { - source = "terraform-google-modules/project-factory/google//modules/fabric-project" - version = "5.0.0" - parent = var.root_node - billing_account = var.billing_account_id - prefix = var.prefix +module "audit-project" { + source = "../../modules/project" name = "audit" - # lien_reason = "audit" - activate_apis = concat(var.project_services, [ + parent = var.root_node + prefix = var.prefix + billing_account = var.billing_account_id + iam_members = { + "roles/bigquery.dataEditor" = [module.audit-log-sinks.writer_identities[0]] + "roles/viewer" = var.iam_audit_viewers + } + iam_roles = [ + "roles/bigquery.dataEditor", + "roles/viewer" + ] + services = concat(var.project_services, [ "bigquery.googleapis.com", ]) - viewers = var.audit_viewers } -# audit logs destination on BigQuery +# audit logs dataset and sink -module "bq-audit-export" { - source = "terraform-google-modules/log-export/google//modules/bigquery" - version = "3.2.0" - project_id = module.project-audit.project_id - dataset_name = "logs_audit_${replace(var.environments[0], "-", "_")}" - log_sink_writer_identity = module.log-sink-audit.writer_identity +module "audit-datasets" { + source = "../../modules/bigquery" + project_id = module.audit-project.project_id + datasets = { + audit_export = { + name = "Audit logs export." + description = "Terraform managed." + location = "EU" + labels = null + options = null + } + } } -# audit log sink -# set the organization as parent to export audit logs for all environments - -module "log-sink-audit" { - source = "terraform-google-modules/log-export/google" - version = "3.2.0" - filter = var.audit_filter - log_sink_name = "logs-audit-${var.environments[0]}" - parent_resource_type = "folder" - parent_resource_id = split("/", module.folders-top-level.ids_list[0])[1] - include_children = "true" - unique_writer_identity = "true" - destination_uri = "${module.bq-audit-export.destination_uri}" +module "audit-log-sinks" { + source = "../../modules/logging-sinks" + parent = var.root_node + destinations = { + audit-logs = "bigquery.googleapis.com/projects/${module.audit-project.project_id}/datasets/${try(module.audit-datasets.names[0], "")}" + } + sinks = { + audit-logs = var.audit_filter + } } ############################################################################### @@ -146,16 +157,19 @@ module "log-sink-audit" { # shared resources project # see the README file for additional options on managing shared services -module "project-shared-resources" { - source = "terraform-google-modules/project-factory/google//modules/fabric-project" - version = "5.0.0" - parent = var.root_node - billing_account = var.billing_account_id - prefix = var.prefix - name = "shared" - activate_apis = var.project_services - extra_bindings_roles = var.shared_bindings_roles - extra_bindings_members = var.shared_bindings_members +module "sharedsvc-project" { + source = "../../modules/project" + name = "sharedsvc" + parent = var.root_node + prefix = var.prefix + billing_account = var.billing_account_id + iam_additive_members = { + "roles/owner" = var.iam_sharedsvc_owners + } + iam_additive_roles = [ + "roles/owner" + ] + services = var.project_services } # Add further modules here for resources that are common to all environments diff --git a/foundations/environments/outputs.tf b/foundations/environments/outputs.tf index cca71987d..f083d1c8a 100644 --- a/foundations/environments/outputs.tf +++ b/foundations/environments/outputs.tf @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -output "terraform_project" { +/* output "terraform_project" { description = "Project that holds the base Terraform resources." value = module.project-tf.project_id } @@ -60,3 +60,4 @@ output "shared_resources_project" { # Add further outputs here for the additional modules that manage shared # resources, like GCR, GCS buckets, KMS, etc. + */ diff --git a/foundations/environments/variables.tf b/foundations/environments/variables.tf index 720b6333a..482e5da4c 100644 --- a/foundations/environments/variables.tf +++ b/foundations/environments/variables.tf @@ -22,14 +22,8 @@ variable "audit_filter" { END } -variable "audit_viewers" { - description = "Audit project viewers, in IAM format." - type = list(string) - default = [] -} - variable "billing_account_id" { - description = "Billing account id used as default for new projects." + description = "Billing account id used as to create projects." type = string } @@ -38,32 +32,79 @@ variable "environments" { type = list(string) } -variable "generate_service_account_keys" { - description = "Generate and store service account keys in the state file." - type = bool - default = false -} - variable "gcs_location" { description = "GCS bucket location." type = string default = "EU" } -variable "grant_xpn_org_roles" { - description = "Grant roles needed for Shared VPC creation to service accounts at the organization level." - type = bool - default = true +variable "iam_assets_editors" { + description = "Shared assets project editors, in IAM format." + type = list(string) + default = [] } -variable "grant_xpn_folder_roles" { - description = "Grant roles needed for Shared VPC creation to service accounts at the environment folder level." - type = bool - default = false +variable "iam_assets_owners" { + description = "Shared assets project owners, in IAM format." + type = list(string) + default = [] +} + +variable "iam_audit_viewers" { + description = "Audit project viewers, in IAM format." + type = list(string) + default = [] +} + +variable "iam_billing_config" { + description = "Control granting billing user role to service accounts. Target the billing account by default." + type = object({ + grant = bool + target_org = bool + }) + default = { + grant = true + target_org = false + } +} + +variable "iam_folder_roles" { + description = "List of roles granted to each service account on its respective folder (excluding XPN roles)." + type = list(string) + default = [ + "roles/compute.networkAdmin", + "roles/owner", + "roles/resourcemanager.folderViewer", + "roles/resourcemanager.projectCreator", + ] +} + +variable "iam_sharedsvc_owners" { + description = "Shared services project owners, in IAM format." + type = list(string) + default = [] +} + +variable "iam_terraform_owners" { + description = "Terraform project owners, in IAM format." + type = list(string) + default = [] +} + +variable "iam_xpn_config" { + description = "Control granting Shared VPC creation roles to service accounts. Target the root node by default." + type = object({ + grant = bool + target_org = bool + }) + default = { + grant = true + target_org = true + } } variable "organization_id" { - description = "Organization id." + description = "Organization id in organizations/nnnnnnnn format." type = string } @@ -77,25 +118,6 @@ variable "root_node" { type = string } -variable "shared_bindings_members" { - description = "List of comma-delimited IAM-format members for the additional shared project bindings." - # example: ["user:a@example.com,b@example.com", "user:c@example.com"] - type = list(string) - default = [] -} -variable "shared_bindings_roles" { - description = "List of roles for additional shared project bindings." - # example: ["roles/storage.objectViewer", "roles/storage.admin"] - type = list(string) - default = [] -} - -variable "terraform_owners" { - description = "Terraform project owners, in IAM format." - type = list(string) - default = [] -} - variable "project_services" { description = "Service APIs enabled by default in new projects." type = list(string) @@ -104,3 +126,9 @@ variable "project_services" { "stackdriver.googleapis.com", ] } + +variable "service_account_keys" { + description = "Generate and store service account keys in the state file." + type = bool + default = true +} diff --git a/foundations/environments/versions.tf b/foundations/environments/versions.tf index 4eb1500c5..92787679e 100644 --- a/foundations/environments/versions.tf +++ b/foundations/environments/versions.tf @@ -13,5 +13,5 @@ # limitations under the License. terraform { - required_version = ">= 0.12" + required_version = ">= 0.12.6" } diff --git a/infrastructure/README.md b/infrastructure/README.md index e393e1108..7c91c87dd 100644 --- a/infrastructure/README.md +++ b/infrastructure/README.md @@ -1,3 +1,33 @@ -# Infrastructure samples +# Networking and infrastructure examples -These examples showcase typical networking configurations on GCP derived from real-world use cases, and are meant to illustrate how to automate them with Terraform, and to offer an easy way of testing different scenarios. We have a long list of examples we plan on adding, so check back here often. \ No newline at end of file +The examples in this folder implement **typical network topologies** like hub and spoke, or **end-to-end scenarios** that allow testing specific features like on-premises DNS policies and Private Google Access. + +They are meant to be used as minimal but complete strting points to create actual infrastructure, and as playgrounds to experiment with specific Google Cloud features. + +## Examples + +### Hub and Spoke via Peering + + This [example](./hub-and-spoke-peering/) implements a hub and spoke topology via VPC peering, a common design where a landing zone VPC (hub) is conncted to on-premises, and then peered with satellite VPCs (spokes) to further partition the infrastructure. + +The sample highlights the lack of transitivity in peering: the absence of connectivity between spokes, and the need create workarounds for private service access to managed services. One such workarund is shown for private GKE, allowing access from hub and all spokes to GKE masters via a dedicated VPN. +
+ +### Hub and Spoke via Dynamic VPN + + This [example](./hub-and-spoke-vpn/) implements a hub and spoke topology via dynamic VPN tunnels, a common design where peering cannot be used due to limitations on the number of spokes or connectivity to managed services. + +The example shows how to implement spoke transitivity via BGP advertisements, how to expose hub DNS zones to spokes via DNS peering, and allows easy testing of different VPN and BGP configurations. +
+ +### DNS and Private Access for On-premises + + This [example](./onprem-google-access-dns/) uses an emulated on-premises environment running in Docker containers inside a GCE instance, to allow testing specific features like DNS policies, DNS forwarding zones across VPN, and Private Access for On-premises hosts. + +The emulated on-premises environment can be used to test access to different services from outside Google Cloud, by implementing a VPN connection and BGP to Google CLoud via Strongswan and Bird. +
+ +### Shared VPC with GKE and per-subnet support + This [example](./shared-vpc-gke/) shows how to configure a Shared VPC, including the specific IAM configurations needed for GKE, and to give different level of access to the VPC subnets to different identities. + +It is meant to be used as a starting point for most Shared VPC configurations, and to be integrated to the above examples where Shared VPC is needed in more complex network topologies. diff --git a/infrastructure/hub-and-spoke-peering/README.md b/infrastructure/hub-and-spoke-peering/README.md new file mode 100644 index 000000000..6e4cb87cd --- /dev/null +++ b/infrastructure/hub-and-spoke-peering/README.md @@ -0,0 +1,75 @@ +# Hub and Spoke via VPC Peering + +This example creates a simple **Hub and Spoke** setup, where the VPC network connects satellite locations (spokes) through a single intermediary location (hub) via [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering). + +The example shows some of the limitations that need to be taken into account when using VPC Peering, mostly due to the lack of transivity between peerings: + +- no mesh networking between the spokes +- complex support for managed services hosted in tenant VPCs connected via peering (Cloud SQL, GKE, etc.) + +One possible solution to the managed service limitation above is presented here, using a static VPN to establish connectivity to the GKE masters in the tenant project ([courtesy of @drebes](https://github.com/drebes/tf-samples/blob/master/gke-master-from-hub/main.tf#L10)). Other solutions typically involve the use of proxies, as [described in this GKE article](https://cloud.google.com/solutions/creating-kubernetes-engine-private-clusters-with-net-proxies). + +One other topic that needs to be considered when using peering is the limit of 25 peerings in each peering group, which constrains the scalability of design like the one presented here. + +The example has been purposefully kept simple to show how to use and wire the VPC modules together, and so that it can be used as a basis for more complex scenarios. This is the high level diagram: + +![High-level diagram](diagram.png "High-level diagram") + +## Managed resources and services + +This sample creates several distinct groups of resources: + +- one VPC each for hub and each spoke +- one set of firewall rules for each VPC +- one Cloud NAT configuration for each spoke +- one test instance for each spoke +- one GKE cluster with a single nodepool in spoke 2 +- one service account for the GCE instances +- one service account for the GKE nodes +- one static VPN gateway in hub and spoke 2 with a single tunnel each + +## Testing GKE access from spoke 1 + +As mentioned above, a VPN tunnel is used as a workaround to avoid the peering transitivity issue that would prevent any VPC other than spoke 2 to connect to the GKE master. + +To test cluster access, first log on to the spoke 2 instance and confirm cluster and IAM roles are set up correctly: + +```bash +gcloud container clusters get-credentials cluster-1 --zone europe-west1-b +kubectl get all +``` + +The next step is to edit the peering towards the GKE master tenant VPC, and enable export routes. The peering has a name like `gke-xxxxxxxxxxxxxxxxxxxx-xxxx-xxxx-peer`, you can edit it in the Cloud Console from the *VPC network peering* page or using `gcloud`: + +``` +gcloud compute networks peerings list +# find the gke-xxxxxxxxxxxxxxxxxxxx-xxxx-xxxx-peer in the spoke-2 network +gcloud compute networks peerings update [peering name from above] \ + --network spoke-2 --export-custom-routes +``` + +Then connect via SSH to the spoke 1 instance and run the same commands you ran on the spoke 2 instance above, you should be able to run `kubectl` commands against the cluster. To test the default situation with no supporting VPN, just comment out the two VPN modules in `main.tf` and run `terraform apply` to bring down the VPN gateways and tunnels. GKE should only become accessible from spoke 2. + +## Operational considerations + +A single pre-existing project is used in this example to keep variables and complexity to a minimum, in a real world scenarios each spoke would probably use a separate project. + +The VPN used to connect the GKE masters VPC does not account for HA, upgrading to use HA VPN is reasonably simple by using the relevant [module](../../modules/net-vpn-ha). + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| project_id | Project id for all resources. | string | ✓ | | +| *ip_ranges* | IP CIDR ranges. | map(string) | | ... | +| *ip_secondary_ranges* | Secondary IP CIDR ranges. | map(string) | | ... | +| *private_service_ranges* | Private service IP CIDR ranges. | map(string) | | ... | +| *region* | VPC regions. | string | | europe-west1 | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| vms | GCE VMs. | | + diff --git a/tests/infrastructure/__init__.py b/infrastructure/hub-and-spoke-peering/backend.tf.sample similarity index 86% rename from tests/infrastructure/__init__.py rename to infrastructure/hub-and-spoke-peering/backend.tf.sample index 086a24e64..a540c7cd1 100644 --- a/tests/infrastructure/__init__.py +++ b/infrastructure/hub-and-spoke-peering/backend.tf.sample @@ -1,4 +1,4 @@ -# Copyright 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,3 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + + +terraform { + backend "gcs" { + bucket = "" + } +} diff --git a/infrastructure/hub-and-spoke-peering/diagram.png b/infrastructure/hub-and-spoke-peering/diagram.png new file mode 100644 index 000000000..0c598fe56 Binary files /dev/null and b/infrastructure/hub-and-spoke-peering/diagram.png differ diff --git a/infrastructure/hub-and-spoke-peering/main.tf b/infrastructure/hub-and-spoke-peering/main.tf new file mode 100644 index 000000000..9f4b87573 --- /dev/null +++ b/infrastructure/hub-and-spoke-peering/main.tf @@ -0,0 +1,280 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.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. + +locals { + vm-instances = concat( + module.vm-spoke-1.instances, + module.vm-spoke-2.instances + ) + vm-startup-script = join("\n", [ + "#! /bin/bash", + "apt-get update && apt-get install -y bash-completion dnsutils kubectl" + ]) +} + +################################################################################ +# Hub networking # +################################################################################ + +module "vpc-hub" { + source = "../../modules/net-vpc" + project_id = var.project_id + name = "hub" + subnets = { + default = { + ip_cidr_range = var.ip_ranges.hub + region = var.region + secondary_ip_range = {} + } + } +} + +module "vpc-hub-firewall" { + source = "../../modules/net-vpc-firewall" + project_id = var.project_id + network = module.vpc-hub.name + admin_ranges_enabled = true + admin_ranges = values(var.ip_ranges) +} + +################################################################################ +# Spoke 1 networking # +################################################################################ + +module "vpc-spoke-1" { + source = "../../modules/net-vpc" + project_id = var.project_id + name = "spoke-1" + subnets = { + default = { + ip_cidr_range = var.ip_ranges.spoke-1 + region = var.region + secondary_ip_range = {} + } + } +} + +module "vpc-spoke-1-firewall" { + source = "../../modules/net-vpc-firewall" + project_id = var.project_id + network = module.vpc-spoke-1.name + admin_ranges_enabled = true + admin_ranges = values(var.ip_ranges) +} + +module "nat-spoke-1" { + source = "../../modules/net-cloudnat" + project_id = var.project_id + region = module.vpc-spoke-1.subnet_regions.default + name = "spoke-1" + router_name = "spoke-1" + router_network = module.vpc-spoke-1.self_link +} + +module "hub-to-spoke-1-peering" { + source = "../../modules/net-vpc-peering" + local_network = module.vpc-hub.self_link + peer_network = module.vpc-spoke-1.self_link + export_local_custom_routes = true + export_peer_custom_routes = false +} + +################################################################################ +# Spoke 2 networking # +################################################################################ + +module "vpc-spoke-2" { + source = "../../modules/net-vpc" + project_id = var.project_id + name = "spoke-2" + subnets = { + default = { + ip_cidr_range = var.ip_ranges.spoke-2 + region = var.region + secondary_ip_range = { + pods = var.ip_secondary_ranges.spoke-2-pods + services = var.ip_secondary_ranges.spoke-2-services + } + } + } +} + +module "vpc-spoke-2-firewall" { + source = "../../modules/net-vpc-firewall" + project_id = var.project_id + network = module.vpc-spoke-2.name + admin_ranges_enabled = true + admin_ranges = values(var.ip_ranges) +} + +module "nat-spoke-2" { + source = "../../modules/net-cloudnat" + project_id = var.project_id + region = module.vpc-spoke-2.subnet_regions.default + name = "spoke-2" + router_name = "spoke-2" + router_network = module.vpc-spoke-2.self_link +} + +module "hub-to-spoke-2-peering" { + source = "../../modules/net-vpc-peering" + local_network = module.vpc-hub.self_link + peer_network = module.vpc-spoke-2.self_link + export_local_custom_routes = true + export_peer_custom_routes = false + module_depends_on = [module.hub-to-spoke-1-peering.complete] +} + +################################################################################ +# Test VMs # +################################################################################ + +module "vm-spoke-1" { + source = "../../modules/compute-vm" + project_id = var.project_id + region = module.vpc-spoke-1.subnet_regions.default + zone = "${module.vpc-spoke-1.subnet_regions.default}-b" + name = "spoke-1-test" + network_interfaces = [{ + network = module.vpc-spoke-1.self_link, + subnetwork = module.vpc-spoke-1.subnet_self_links.default, + nat = false, + addresses = null + }] + metadata = { startup-script = local.vm-startup-script } + service_account = module.service-account-gce.email + service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"] + tags = ["ssh"] +} + +module "vm-spoke-2" { + source = "../../modules/compute-vm" + project_id = var.project_id + region = module.vpc-spoke-2.subnet_regions.default + zone = "${module.vpc-spoke-2.subnet_regions.default}-b" + name = "spoke-2-test" + network_interfaces = [{ + network = module.vpc-spoke-2.self_link, + subnetwork = module.vpc-spoke-2.subnet_self_links.default, + nat = false, + addresses = null + }] + metadata = { startup-script = local.vm-startup-script } + service_account = module.service-account-gce.email + service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"] + tags = ["ssh"] +} + +module "service-account-gce" { + source = "../../modules/iam-service-accounts" + project_id = var.project_id + names = ["gce-test"] + iam_project_roles = { + (var.project_id) = [ + "roles/container.developer", + "roles/logging.logWriter", + "roles/monitoring.metricWriter", + ] + } +} + +################################################################################ +# GKE # +################################################################################ + +module "cluster-1" { + source = "../../modules/gke-cluster" + name = "cluster-1" + project_id = var.project_id + location = "${module.vpc-spoke-2.subnet_regions.default}-b" + network = module.vpc-spoke-2.self_link + subnetwork = module.vpc-spoke-2.subnet_self_links.default + secondary_range_pods = "pods" + secondary_range_services = "services" + default_max_pods_per_node = 32 + labels = { + environment = "test" + } + master_authorized_ranges = { + for name, range in var.ip_ranges : name => range + } + private_cluster_config = { + enable_private_nodes = true + enable_private_endpoint = true + master_ipv4_cidr_block = var.private_service_ranges.spoke-2-cluster-1 + } +} + +module "cluster-1-nodepool-1" { + source = "../../modules/gke-nodepool" + name = "nodepool-1" + project_id = var.project_id + location = module.cluster-1.location + cluster_name = module.cluster-1.name + node_config_service_account = module.service-account-gke-node.email +} + +# roles assigned via this module use non-authoritative IAM bindings at the +# project level, with no risk of conflicts with pre-existing roles + +module "service-account-gke-node" { + source = "../../modules/iam-service-accounts" + project_id = var.project_id + names = ["gke-node"] + iam_project_roles = { + (var.project_id) = [ + "roles/logging.logWriter", "roles/monitoring.metricWriter", + ] + } +} + +################################################################################ +# GKE peering VPN # +################################################################################ + +module "vpn-hub" { + source = "../../modules/net-vpn-static" + project_id = var.project_id + region = var.region + network = module.vpc-hub.name + name = "hub" + remote_ranges = values(var.private_service_ranges) + tunnels = { + spoke-2 = { + ike_version = 2 + peer_ip = module.vpn-spoke-2.address + shared_secret = "" + traffic_selectors = { local = ["0.0.0.0/0"], remote = null } + } + } +} + +module "vpn-spoke-2" { + source = "../../modules/net-vpn-static" + project_id = var.project_id + region = var.region + network = module.vpc-spoke-2.name + name = "spoke-2" + # use an aggregate of the remote ranges, so as to be less specific than the + # routes exchanged via peering + remote_ranges = ["10.0.0.0/8"] + tunnels = { + spoke-2 = { + ike_version = 2 + peer_ip = module.vpn-hub.address + shared_secret = module.vpn-hub.random_secret + traffic_selectors = { local = ["0.0.0.0/0"], remote = null } + } + } +} diff --git a/infrastructure/hub-and-spoke-peering/outputs.tf b/infrastructure/hub-and-spoke-peering/outputs.tf new file mode 100644 index 000000000..933d03cf3 --- /dev/null +++ b/infrastructure/hub-and-spoke-peering/outputs.tf @@ -0,0 +1,21 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +output "vms" { + description = "GCE VMs." + value = { + for instance in local.vm-instances : + instance.name => instance.network_interface.0.network_ip + } +} diff --git a/infrastructure/hub-and-spoke-peering/variables.tf b/infrastructure/hub-and-spoke-peering/variables.tf new file mode 100644 index 000000000..3f1375b2e --- /dev/null +++ b/infrastructure/hub-and-spoke-peering/variables.tf @@ -0,0 +1,51 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +variable "ip_ranges" { + description = "IP CIDR ranges." + type = map(string) + default = { + hub = "10.0.0.0/24" + spoke-1 = "10.0.16.0/24" + spoke-2 = "10.0.32.0/24" + } +} + +variable "ip_secondary_ranges" { + description = "Secondary IP CIDR ranges." + type = map(string) + default = { + spoke-2-pods = "10.128.0.0/18" + spoke-2-services = "172.16.0.0/24" + } +} + +variable "private_service_ranges" { + description = "Private service IP CIDR ranges." + type = map(string) + default = { + spoke-2-cluster-1 = "192.168.0.0/28" + } +} + +variable "project_id" { + description = "Project id for all resources." + type = string +} + +variable "region" { + description = "VPC region." + type = string + default = "europe-west1" +} diff --git a/infrastructure/hub-and-spoke-peering/versions.tf b/infrastructure/hub-and-spoke-peering/versions.tf new file mode 100644 index 000000000..6eed2e875 --- /dev/null +++ b/infrastructure/hub-and-spoke-peering/versions.tf @@ -0,0 +1,25 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.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 = ">= 0.12.16" +} + +provider "google" { + version = "~> 3.3" +} + +provider "google-beta" { + version = "~> 3.3" +} diff --git a/infrastructure/hub-and-spoke-vpn/README.md b/infrastructure/hub-and-spoke-vpn/README.md new file mode 100644 index 000000000..2e39e68cb --- /dev/null +++ b/infrastructure/hub-and-spoke-vpn/README.md @@ -0,0 +1,54 @@ +# Hub and Spoke via VPN + +This example creates a simple **Hub and Spoke VPN** setup, where the VPC network connects satellite locations (spokes) through a single intermediary location (hub) via [IPsec VPN](https://cloud.google.com/vpn/docs/concepts/overview), optionally providing full-mesh networking via [custom route advertisements](https://cloud.google.com/router/docs/how-to/advertising-overview). + +The example has been purposefully kept simple to show how to use and wire the VPC and VPN modules together, and so that it can be used as a basis for more complex scenarios. This is the high level diagram: + +![High-level diagram](diagram.png "High-level diagram") + +## Managed resources and services + +This sample creates several distinct groups of resources: + +- one VPC for each hub and each spoke +- one set of firewall rules for each VPC +- one VPN gateway, one tunnel and one Cloud Router for each spoke +- two VPN gateways, two tunnels and two Cloud Routers for the hub (one for each spoke) +- one DNS private zone in the hub +- one DNS peering zone in each spoke +- one Cloud NAT configuration for each spoke +- one test instance for each spoke + +## Operational considerations + +A single pre-existing project is used in this example to keep variables and complexity to a minimum, in a real world scenarios each spoke would probably use a separate project. The provided project needs a valid billing account and the Compute and DNS APIs enabled. You can easily create such a project with the [project module](../../modules/project) or with the following commands: + +``` shell +MY_PROJECT_ID="" +gcloud projects create $MY_PROJECT_ID +gcloud alpha billing projects link --billing-account=XXXXXX-XXXXXX-XXXXXX $MY_PROJECT_ID +gcloud services enable --project=$MY_PROJECT_ID {compute,dns}.googleapis.com +``` + +The example does not account for HA, but the VPN gateways can be easily upgraded to use HA VPN via the [net-vpn-ha module](../../modules/net-vpn-ha). + +If a single router and VPN gateway are used in the hub to manage all tunnels, particular care must be taken in announcing ranges from hub to spokes, as Cloud Router does not explicitly support transitivity and overlapping routes received from both sides create unintended side effects. The simple workaround is to announce a single aggregated route from hub to spokes so that it does not overlap with any of the ranges advertised by each spoke to the hub. + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| project_id | Project id for all resources. | string | ✓ | | +| *bgp_asn* | BGP ASNs. | map(number) | | ... | +| *bgp_custom_advertisements* | BGP custom advertisement IP CIDR ranges. | map(string) | | ... | +| *bgp_interface_ranges* | None | | | ... | +| *ip_ranges* | IP CIDR ranges. | map(string) | | ... | +| *regions* | VPC regions. | map(string) | | ... | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| vms | GCE VMs. | | + diff --git a/infrastructure/hub-and-spoke-vpns/backend.tf.sample b/infrastructure/hub-and-spoke-vpn/backend.tf.sample similarity index 100% rename from infrastructure/hub-and-spoke-vpns/backend.tf.sample rename to infrastructure/hub-and-spoke-vpn/backend.tf.sample diff --git a/infrastructure/hub-and-spoke-vpn/diagram.png b/infrastructure/hub-and-spoke-vpn/diagram.png new file mode 100644 index 000000000..6d5dc5ff2 Binary files /dev/null and b/infrastructure/hub-and-spoke-vpn/diagram.png differ diff --git a/infrastructure/hub-and-spoke-vpn/main.tf b/infrastructure/hub-and-spoke-vpn/main.tf new file mode 100644 index 000000000..bee66f7fb --- /dev/null +++ b/infrastructure/hub-and-spoke-vpn/main.tf @@ -0,0 +1,308 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.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. + +locals { + vm-instances = concat( + module.vm-spoke-1.instances, module.vm-spoke-2.instances + ) + vm-startup-script = join("\n", [ + "#! /bin/bash", + "apt-get update && apt-get install -y dnsutils" + ]) +} + +################################################################################ +# Hub networking # +################################################################################ + +module "vpc-hub" { + source = "../../modules/net-vpc" + project_id = var.project_id + name = "hub" + subnets = { + a = { + ip_cidr_range = var.ip_ranges.hub-a + region = var.regions.a + secondary_ip_range = {} + } + b = { + ip_cidr_range = var.ip_ranges.hub-b + region = var.regions.b + secondary_ip_range = {} + } + } +} + +module "vpc-hub-firewall" { + source = "../../modules/net-vpc-firewall" + project_id = var.project_id + network = module.vpc-hub.name + admin_ranges_enabled = true + admin_ranges = values(var.ip_ranges) +} + +module "vpn-hub-a" { + source = "../../modules/net-vpn-dynamic" + project_id = var.project_id + region = module.vpc-hub.subnet_regions["a"] + network = module.vpc-hub.name + name = "hub-a" + router_asn = var.bgp_asn.hub + tunnels = { + spoke-1 = { + bgp_peer = { + address = cidrhost(var.bgp_interface_ranges.spoke-1, 2) + asn = var.bgp_asn.spoke-1 + } + bgp_peer_options = { + advertise_groups = ["ALL_SUBNETS"] + advertise_ip_ranges = { + (var.bgp_custom_advertisements.hub-to-spoke-1) = "spoke-2" + } + advertise_mode = "CUSTOM" + route_priority = 1000 + } + bgp_session_range = "${cidrhost(var.bgp_interface_ranges.spoke-1, 1)}/30" + ike_version = 2 + peer_ip = module.vpn-spoke-1.address + shared_secret = "" + } + } +} + +module "vpn-hub-b" { + source = "../../modules/net-vpn-dynamic" + project_id = var.project_id + region = module.vpc-hub.subnet_regions["b"] + network = module.vpc-hub.name + name = "hub-b" + router_asn = var.bgp_asn.hub + tunnels = { + spoke-2 = { + bgp_peer = { + address = cidrhost(var.bgp_interface_ranges.spoke-2, 2) + asn = var.bgp_asn.spoke-2 + } + bgp_peer_options = { + advertise_groups = ["ALL_SUBNETS"] + advertise_ip_ranges = { + (var.bgp_custom_advertisements.hub-to-spoke-2) = "spoke-1" + } + advertise_mode = "CUSTOM" + route_priority = 1000 + } + bgp_session_range = "${cidrhost(var.bgp_interface_ranges.spoke-2, 1)}/30" + ike_version = 2 + peer_ip = module.vpn-spoke-2.address + shared_secret = "" + } + } +} + +################################################################################ +# Spoke 1 networking # +################################################################################ + +module "vpc-spoke-1" { + source = "../../modules/net-vpc" + project_id = var.project_id + name = "spoke-1" + subnets = { + a = { + ip_cidr_range = var.ip_ranges.spoke-1-a + region = var.regions.a + secondary_ip_range = {} + } + b = { + ip_cidr_range = var.ip_ranges.spoke-1-b + region = var.regions.a + secondary_ip_range = {} + } + } +} + +module "vpc-spoke-1-firewall" { + source = "../../modules/net-vpc-firewall" + project_id = var.project_id + network = module.vpc-spoke-1.name + admin_ranges_enabled = true + admin_ranges = values(var.ip_ranges) +} + +module "vpn-spoke-1" { + source = "../../modules/net-vpn-dynamic" + project_id = var.project_id + region = module.vpc-spoke-1.subnet_regions["a"] + network = module.vpc-spoke-1.name + name = "spoke-1" + router_asn = var.bgp_asn.spoke-1 + tunnels = { + hub = { + bgp_peer = { + address = cidrhost(var.bgp_interface_ranges.spoke-1, 1) + asn = var.bgp_asn.hub + } + bgp_peer_options = null + bgp_session_range = "${cidrhost(var.bgp_interface_ranges.spoke-1, 2)}/30" + ike_version = 2 + peer_ip = module.vpn-hub-a.address + shared_secret = module.vpn-hub-a.random_secret + } + } +} + +module "nat-spoke-1" { + source = "../../modules/net-cloudnat" + project_id = var.project_id + region = module.vpc-spoke-1.subnet_regions["a"] + name = "spoke-1" + router_create = false + router_name = module.vpn-spoke-1.router_name +} + +################################################################################ +# Spoke 2 networking # +################################################################################ + +module "vpc-spoke-2" { + source = "../../modules/net-vpc" + project_id = var.project_id + name = "spoke-2" + subnets = { + a = { + ip_cidr_range = var.ip_ranges.spoke-2-a + region = var.regions.b + secondary_ip_range = {} + } + b = { + ip_cidr_range = var.ip_ranges.spoke-2-b + region = var.regions.b + secondary_ip_range = {} + } + } +} + +module "vpc-spoke-2-firewall" { + source = "../../modules/net-vpc-firewall" + project_id = var.project_id + network = module.vpc-spoke-2.name + admin_ranges_enabled = true + admin_ranges = values(var.ip_ranges) +} + +module "vpn-spoke-2" { + source = "../../modules/net-vpn-dynamic" + project_id = var.project_id + region = module.vpc-spoke-2.subnet_regions["a"] + network = module.vpc-spoke-2.name + name = "spoke-2" + router_asn = var.bgp_asn.spoke-2 + tunnels = { + hub = { + bgp_peer = { + address = cidrhost(var.bgp_interface_ranges.spoke-2, 1) + asn = var.bgp_asn.hub + } + bgp_peer_options = null + bgp_session_range = "${cidrhost(var.bgp_interface_ranges.spoke-2, 2)}/30" + ike_version = 2 + peer_ip = module.vpn-hub-b.address + shared_secret = module.vpn-hub-b.random_secret + } + } +} + +module "nat-spoke-2" { + source = "../../modules/net-cloudnat" + project_id = var.project_id + region = module.vpc-spoke-2.subnet_regions["a"] + name = "spoke-2" + router_create = false + router_name = module.vpn-spoke-2.router_name +} + +################################################################################ +# Test VMs # +################################################################################ + +module "vm-spoke-1" { + source = "../../modules/compute-vm" + project_id = var.project_id + region = module.vpc-spoke-1.subnet_regions.b + zone = "${module.vpc-spoke-1.subnet_regions.b}-b" + name = "spoke-1-test" + network_interfaces = [{ + network = module.vpc-spoke-1.self_link, + subnetwork = module.vpc-spoke-1.subnet_self_links.b, + nat = false, + addresses = null + }] + tags = ["ssh"] + metadata = { startup-script = local.vm-startup-script } +} + +module "vm-spoke-2" { + source = "../../modules/compute-vm" + project_id = var.project_id + region = module.vpc-spoke-2.subnet_regions.b + zone = "${module.vpc-spoke-2.subnet_regions.b}-b" + name = "spoke-2-test" + network_interfaces = [{ + network = module.vpc-spoke-2.self_link, + subnetwork = module.vpc-spoke-2.subnet_self_links.b, + nat = false, + addresses = null + }] + tags = ["ssh"] + metadata = { startup-script = local.vm-startup-script } +} + +################################################################################ +# DNS zones # +################################################################################ + +module "dns-host" { + source = "../../modules/dns" + project_id = var.project_id + type = "private" + name = "example" + domain = "example.com." + client_networks = [module.vpc-hub.self_link] + recordsets = [ + for instance in local.vm-instances : { + name = instance.name, type = "A", ttl = 300, + records = [instance.network_interface.0.network_ip] + } + ] +} + +module "dns-spoke-1" { + source = "../../modules/dns" + project_id = var.project_id + type = "peering" + name = "spoke-1" + domain = "example.com." + client_networks = [module.vpc-spoke-1.self_link] + peer_network = module.vpc-hub.self_link +} + +module "dns-spoke-2" { + source = "../../modules/dns" + project_id = var.project_id + type = "peering" + name = "spoke-2" + domain = "example.com." + client_networks = [module.vpc-spoke-2.self_link] + peer_network = module.vpc-hub.self_link +} diff --git a/infrastructure/hub-and-spoke-vpn/outputs.tf b/infrastructure/hub-and-spoke-vpn/outputs.tf new file mode 100644 index 000000000..0edd756d2 --- /dev/null +++ b/infrastructure/hub-and-spoke-vpn/outputs.tf @@ -0,0 +1,21 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +output "vms" { + description = "GCE VMs." + value = { + for instance in local.vm-instances : + instance.name => instance.network_interface.0.network_ip + } +} diff --git a/infrastructure/hub-and-spoke-vpns/provider.tf b/infrastructure/hub-and-spoke-vpn/provider.tf similarity index 100% rename from infrastructure/hub-and-spoke-vpns/provider.tf rename to infrastructure/hub-and-spoke-vpn/provider.tf diff --git a/infrastructure/hub-and-spoke-vpn/variables.tf b/infrastructure/hub-and-spoke-vpn/variables.tf new file mode 100644 index 000000000..2d3feaf5f --- /dev/null +++ b/infrastructure/hub-and-spoke-vpn/variables.tf @@ -0,0 +1,68 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +variable "bgp_custom_advertisements" { + description = "BGP custom advertisement IP CIDR ranges." + type = map(string) + default = { + hub-to-spoke-1 = "10.0.32.0/20" + hub-to-spoke-2 = "10.0.16.0/20" + } +} + +variable "bgp_asn" { + description = "BGP ASNs." + type = map(number) + default = { + hub = 64513 + spoke-1 = 64514 + spoke-2 = 64515 + } +} + +variable "bgp_interface_ranges" { + description = "BGP interface IP CIDR ranges." + type = map(string) + default = { + spoke-1 = "169.254.1.0/30" + spoke-2 = "169.254.1.4/30" + } +} + +variable "ip_ranges" { + description = "IP CIDR ranges." + type = map(string) + default = { + hub-a = "10.0.0.0/24" + hub-b = "10.0.8.0/24" + spoke-1-a = "10.0.16.0/24" + spoke-1-b = "10.0.24.0/24" + spoke-2-a = "10.0.32.0/24" + spoke-2-b = "10.0.40.0/24" + } +} + +variable "project_id" { + description = "Project id for all resources." + type = string +} + +variable "regions" { + description = "VPC regions." + type = map(string) + default = { + a = "europe-west1" + b = "europe-west2" + } +} diff --git a/infrastructure/hub-and-spoke-vpns/README.md b/infrastructure/hub-and-spoke-vpns/README.md deleted file mode 100644 index adbc4fcd3..000000000 --- a/infrastructure/hub-and-spoke-vpns/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# Hub and Spoke VPNs - -This sample creates a simple **Hub and Spoke VPN** architecture, where the VPC network connects satellite locations (spokes) through a single intermediary location (hub) via [IPsec VPN](https://cloud.google.com/vpn/docs/concepts/overview), optionally providing full-mesh networking via [custom route advertisements](https://cloud.google.com/router/docs/how-to/advertising-overview). - -> **NOTE**: This example is not designed to provide HA, please refer to the [documentation](https://cloud.google.com/vpn/docs/concepts/advanced#ha-options) for information on Cloud VPNs and HA. - - -The benefits of this topology include: - -- Network/Security Admin manages Central Services Project (Hub). -- Central services and tools deployed in Central Services Project (Hub) for use by all Service Projects (Spokes). -- Network/Security Admin hands over spoke Projects to respective team who then have full autonomy. -- Network/Security Admin monitors spoke projects for organization security posture compliance using tools like [Forseti](https://forsetisecurity.org/), [CSCC](https://cloud.google.com/security-command-center/) etc deployed in Central Services Project (Hub). -- Spokes communicate with on-prem via VPN to transit hub and then over Interconnect or VPN to on-premises (on-premises resources are not included in this sample for obvious reasons). -- (Optional) Spokes communicate in a full-mesh to each other via VPN transit routing in Central Services Project (Hub). -- This is a decentralized architecture where each spoke project has autonomy to manage all their GCP compute and network resources. - -The purpose of this sample is showing how to wire different [Cloud Foundation Fabric](https://github.com/search?q=topic%3Acft-fabric+org%3Aterraform-google-modules&type=Repositories) modules to create **Hub and Spoke VPNs** network architectures, and as such it is meant to be used for prototyping, or to experiment with networking configurations. Additional best practices and security considerations need to be taken into account for real world usage (eg removal of default service accounts, disabling of external IPs, firewall design, etc). - - -![High-level diagram](diagram.png "High-level diagram") - -## Managed resources and services - -This sample creates several distinct groups of resources: - -- three VPC Networks (hub network and two ppoke networks) -- VPC-level resources (VPC, subnets, firewall rules, etc.) -- one Cloud DNS Private zone in the hub project -- one Cloud DNS Forwarding zone in the hub project -- four Cloud DNS Peering zones (two per each spoke project) -- one Cloud DNS Policy for inbound forwarding -- four Cloud Routers (two in hub project and one per each spoke project) -- four Cloud VPNs (two in hub project and one per each spoke project) - -## Test resources - -A set of test resources are included for convenience, as they facilitate experimenting with different networking configurations (firewall rules, external connectivity via VPN, etc.). They are encapsulated in the `test-resources.tf` file, and can be safely removed as a single unit. - -- two virtual machine instances in hub project (one per each region) -- two virtual machine instances in spoke1 project (one per each region) -- two virtual machine instances in spoke2 project (one per each region) - -SSH access to instances is configured via [OS Login](https://cloud.google.com/compute/docs/oslogin/). External access is allowed via the default SSH rule created by the firewall module, and corresponding `ssh` tags on the instances. - -## Known issues - - It is not possible to get inbound DNS forwarding IPs in the terraform output. - - Please refer to the [bug](https://github.com/terraform-providers/terraform-provider-google/issues/3753) for more details. - - Please refer to the [documentation](https://cloud.google.com/dns/zones/#creating_a_dns_policy_that_enables_inbound_dns_forwarding) on how to get the IPs with `gcloud`. - - -## Variables - -| name | description | type | required | default | -|---|---|:---: |:---:|:---:| -| hub_project_id | Hub Project id. Same project can be used for hub and spokes. | string | ✓ | | -| spoke_1_project_id | Spoke 1 Project id. Same project can be used for hub and spokes. | string | ✓ | | -| spoke_2_project_id | Spoke 2 Project id. Same project can be used for hub and spokes. | string | ✓ | | -| *forwarding_dns_zone_domain* | Forwarding DNS Zone Domain. | string | | on-prem.local. | -| *forwarding_dns_zone_name* | Forwarding DNS Zone Name. | string | | on-prem-local | -| *forwarding_zone_server_addresses* | Forwarding DNS Zone Server Addresses | list(string) | | ["8.8.8.8", "8.8.4.4"] | -| *hub_bgp_asn* | Hub BGP ASN. | number | | 64515 | -| *hub_subnets* | Hub VPC subnets configuration. | list(object({...})) | | ... | -| *private_dns_zone_domain* | Private DNS Zone Domain. | string | | gcp.local. | -| *private_dns_zone_name* | Private DNS Zone Name. | string | | gcp-local | -| *spoke_1_bgp_asn* | Spoke 1 BGP ASN. | number | | 64516 | -| *spoke_1_subnets* | Spoke 1 VPC subnets configuration. | | | ... | -| *spoke_2_bgp_asn* | Spoke 2 BGP ASN. | number | | 64517 | -| *spoke_2_subnets* | Spoke 2 VPC subnets configuration. | | | ... | -| *spoke_to_spoke_route_advertisement* | Use custom route advertisement in hub routers to advertise all spoke subnets. | bool | | true | - -## Outputs - -| name | description | sensitive | -|---|---|:---:| -| hub | Hub network resources. | | -| spoke-1 | Spoke1 network resources. | | -| spoke-2 | Spoke2 network resources. | | - diff --git a/infrastructure/hub-and-spoke-vpns/diagram.png b/infrastructure/hub-and-spoke-vpns/diagram.png deleted file mode 100644 index 6e3b9daa1..000000000 Binary files a/infrastructure/hub-and-spoke-vpns/diagram.png and /dev/null differ diff --git a/infrastructure/hub-and-spoke-vpns/main.tf b/infrastructure/hub-and-spoke-vpns/main.tf deleted file mode 100644 index a4dcbaea3..000000000 --- a/infrastructure/hub-and-spoke-vpns/main.tf +++ /dev/null @@ -1,344 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -locals { - hub_subnet_regions = [for subnet in var.hub_subnets : subnet["subnet_region"]] - spoke_1_subnet_regions = [for subnet in var.spoke_1_subnets : subnet["subnet_region"]] - spoke_2_subnet_regions = [for subnet in var.spoke_2_subnets : subnet["subnet_region"]] - hub_subnet_cidr_ranges = [for subnet in var.hub_subnets : subnet["subnet_ip"]] - spoke_1_subnet_cidr_ranges = [for subnet in var.spoke_1_subnets : subnet["subnet_ip"]] - spoke_2_subnet_cidr_ranges = [for subnet in var.spoke_2_subnets : subnet["subnet_ip"]] - all_subnet_cidrs = concat(local.hub_subnet_cidr_ranges, local.spoke_1_subnet_cidr_ranges, local.spoke_2_subnet_cidr_ranges) - hub_to_spoke_1_router = var.spoke_to_spoke_route_advertisement ? element(concat(google_compute_router.hub-to-spoke-1-custom.*.name, list("")), 0) : element(concat(google_compute_router.hub-to-spoke-1-default.*.name, list("")), 0) - hub_to_spoke_2_router = var.spoke_to_spoke_route_advertisement ? element(concat(google_compute_router.hub-to-spoke-2-custom.*.name, list("")), 0) : element(concat(google_compute_router.hub-to-spoke-2-default.*.name, list("")), 0) -} - -############################################################## -# VPCs # -############################################################## - -module "vpc-hub" { - source = "terraform-google-modules/network/google" - version = "~> 1.4.3" - - project_id = var.hub_project_id - network_name = "hub-network" - subnets = var.hub_subnets - routing_mode = "GLOBAL" -} - -module "vpc-spoke-1" { - source = "terraform-google-modules/network/google" - version = "~> 1.4.3" - - project_id = var.spoke_1_project_id - network_name = "spoke-1-network" - subnets = var.spoke_1_subnets - routing_mode = "GLOBAL" -} - -module "vpc-spoke-2" { - source = "terraform-google-modules/network/google" - version = "~> 1.4.3" - - project_id = var.spoke_2_project_id - network_name = "spoke-2-network" - subnets = var.spoke_2_subnets - routing_mode = "GLOBAL" -} - -############################################################## -# Firewalls # -############################################################## - -module "firewall-hub" { - source = "terraform-google-modules/network/google//modules/fabric-net-firewall" - version = "~> 1.2" - - project_id = var.hub_project_id - network = module.vpc-hub.network_name - admin_ranges_enabled = true - admin_ranges = local.all_subnet_cidrs -} - -module "firewall-spoke-1" { - source = "terraform-google-modules/network/google//modules/fabric-net-firewall" - version = "~> 1.2" - - project_id = var.spoke_1_project_id - network = module.vpc-spoke-1.network_name - admin_ranges_enabled = true - admin_ranges = local.all_subnet_cidrs -} - -module "firewall-spoke-2" { - source = "terraform-google-modules/network/google//modules/fabric-net-firewall" - version = "~> 1.2" - - project_id = var.spoke_2_project_id - network = module.vpc-spoke-2.network_name - admin_ranges_enabled = true - admin_ranges = local.all_subnet_cidrs -} - -############################################################## -# Cloud Routers # -############################################################## - -resource "google_compute_router" "hub-to-spoke-1-custom" { - count = var.spoke_to_spoke_route_advertisement ? 1 : 0 - name = "hub-to-spoke-1-custom" - region = element(local.hub_subnet_regions, 0) - network = module.vpc-hub.network_name - project = var.hub_project_id - bgp { - asn = var.hub_bgp_asn - advertise_mode = "CUSTOM" - advertised_groups = ["ALL_SUBNETS"] - - dynamic "advertised_ip_ranges" { - for_each = toset(local.spoke_2_subnet_cidr_ranges) - content { - range = advertised_ip_ranges.value - } - } - } -} - -resource "google_compute_router" "hub-to-spoke-2-custom" { - count = var.spoke_to_spoke_route_advertisement ? 1 : 0 - name = "hub-to-spoke-2-custom" - region = element(local.hub_subnet_regions, 1) - network = module.vpc-hub.network_name - project = var.hub_project_id - bgp { - asn = var.hub_bgp_asn - advertise_mode = "CUSTOM" - advertised_groups = ["ALL_SUBNETS"] - dynamic "advertised_ip_ranges" { - for_each = toset(local.spoke_1_subnet_cidr_ranges) - content { - range = advertised_ip_ranges.value - } - } - } -} - -resource "google_compute_router" "hub-to-spoke-1-default" { - count = var.spoke_to_spoke_route_advertisement ? 0 : 1 - name = "hub-to-spoke-1-default" - region = element(local.hub_subnet_regions, 0) - network = module.vpc-hub.network_name - project = var.hub_project_id - bgp { - asn = var.hub_bgp_asn - } -} -resource "google_compute_router" "hub-to-spoke-2-default" { - count = var.spoke_to_spoke_route_advertisement ? 0 : 1 - name = "hub-to-spoke-2-default" - region = element(local.hub_subnet_regions, 1) - network = module.vpc-hub.network_name - project = var.hub_project_id - bgp { - asn = var.hub_bgp_asn - } -} -resource "google_compute_router" "spoke-1" { - name = "spoke-1" - region = element(local.spoke_1_subnet_regions, 0) - network = module.vpc-spoke-1.network_name - project = var.spoke_1_project_id - bgp { - asn = var.spoke_1_bgp_asn - } -} -resource "google_compute_router" "spoke-2" { - name = "spoke-2" - region = element(local.spoke_2_subnet_regions, 1) - network = module.vpc-spoke-2.network_name - project = var.spoke_2_project_id - bgp { - asn = var.spoke_2_bgp_asn - } -} - -############################################################## -# VPNs # -############################################################## - -module "vpn-hub-to-spoke-1" { - source = "terraform-google-modules/vpn/google" - version = "~> 1.1" - - project_id = var.hub_project_id - network = module.vpc-hub.network_name - region = element(local.hub_subnet_regions, 0) - gateway_name = "hub-to-spoke-1-gtw" - tunnel_name_prefix = "hub-to-spoke-1" - peer_ips = [module.vpn-spoke-1-to-hub.gateway_ip] - bgp_cr_session_range = ["169.254.0.1/30"] - bgp_remote_session_range = ["169.254.0.2"] - peer_asn = [var.spoke_1_bgp_asn] - cr_name = local.hub_to_spoke_1_router -} - -module "vpn-hub-to-spoke-2" { - source = "terraform-google-modules/vpn/google" - version = "~> 1.1" - - project_id = var.hub_project_id - network = module.vpc-hub.network_name - region = element(local.hub_subnet_regions, 1) - gateway_name = "hub-to-spoke-2-gtw" - tunnel_name_prefix = "hub-to-spoke-2" - peer_ips = [module.vpn-spoke-2-to-hub.gateway_ip] - bgp_cr_session_range = ["169.254.1.1/30"] - bgp_remote_session_range = ["169.254.1.2"] - peer_asn = [var.spoke_2_bgp_asn] - cr_name = local.hub_to_spoke_2_router -} - -module "vpn-spoke-1-to-hub" { - source = "terraform-google-modules/vpn/google" - version = "~> 1.1" - - project_id = var.spoke_1_project_id - network = module.vpc-spoke-1.network_name - region = element(local.spoke_1_subnet_regions, 0) - gateway_name = "spoke-1-to-hub-gtw" - tunnel_name_prefix = "spoke-1-to-hub" - shared_secret = module.vpn-hub-to-spoke-1.ipsec_secret-dynamic[0] - peer_ips = [module.vpn-hub-to-spoke-1.gateway_ip] - bgp_cr_session_range = ["169.254.0.2/30"] - bgp_remote_session_range = ["169.254.0.1"] - peer_asn = [var.hub_bgp_asn] - cr_name = google_compute_router.spoke-1.name -} - -module "vpn-spoke-2-to-hub" { - source = "terraform-google-modules/vpn/google" - version = "~> 1.1" - - project_id = var.spoke_2_project_id - network = module.vpc-spoke-2.network_name - region = element(local.spoke_2_subnet_regions, 1) - gateway_name = "spoke-2-to-hub-gtw" - tunnel_name_prefix = "spoke-2-to-hub" - shared_secret = module.vpn-hub-to-spoke-2.ipsec_secret-dynamic[0] - peer_ips = [module.vpn-hub-to-spoke-2.gateway_ip] - bgp_cr_session_range = ["169.254.1.2/30"] - bgp_remote_session_range = ["169.254.1.1"] - peer_asn = [var.hub_bgp_asn] - cr_name = google_compute_router.spoke-2.name -} - -############################################################## -# DNS Zones # -############################################################## - -module "hub-private-zone" { - source = "terraform-google-modules/cloud-dns/google" - version = "~> 2.0" - - project_id = var.hub_project_id - type = "private" - name = "${var.private_dns_zone_name}-hub-private" - domain = var.private_dns_zone_domain - - private_visibility_config_networks = [module.vpc-hub.network_self_link] -} - -module "hub-forwarding-zone" { - source = "terraform-google-modules/cloud-dns/google" - version = "~> 2.0" - - project_id = var.hub_project_id - type = "forwarding" - name = "${var.forwarding_dns_zone_name}-hub-forwarding" - domain = var.forwarding_dns_zone_domain - - private_visibility_config_networks = [module.vpc-hub.network_self_link] - target_name_server_addresses = var.forwarding_zone_server_addresses -} - -module "spoke-1-peering-zone-to-hub-private-zone" { - source = "terraform-google-modules/cloud-dns/google" - version = "~> 2.0" - - project_id = var.spoke_1_project_id - type = "peering" - name = "${var.private_dns_zone_name}-spoke-1-peering-to-hub-private" - domain = var.private_dns_zone_domain - - private_visibility_config_networks = [module.vpc-spoke-1.network_self_link] - target_network = module.vpc-hub.network_self_link -} - -module "spoke-2-peering-zone-to-hub-private-zone" { - source = "terraform-google-modules/cloud-dns/google" - version = "~> 2.0" - - project_id = var.spoke_2_project_id - type = "peering" - name = "${var.private_dns_zone_name}-spoke-2-peering-to-hub-private" - domain = var.private_dns_zone_domain - - private_visibility_config_networks = [module.vpc-spoke-2.network_self_link] - target_network = module.vpc-hub.network_self_link -} - -module "spoke-1-peering-zone-to-hub-forwarding-zone" { - source = "terraform-google-modules/cloud-dns/google" - version = "~> 2.0" - - project_id = var.spoke_1_project_id - type = "peering" - name = "${var.private_dns_zone_name}-spoke-1-peering-to-hub-forwarding" - domain = var.forwarding_dns_zone_domain - - private_visibility_config_networks = [module.vpc-spoke-1.network_self_link] - target_network = module.vpc-hub.network_self_link -} - -module "spoke-2-peering-zone-to-hub-forwarding-zone" { - source = "terraform-google-modules/cloud-dns/google" - version = "~> 2.0" - - project_id = var.spoke_2_project_id - type = "peering" - name = "${var.private_dns_zone_name}-spoke-2-peering-to-hub-forwarding" - domain = var.forwarding_dns_zone_domain - - private_visibility_config_networks = [module.vpc-spoke-2.network_self_link] - target_network = module.vpc-hub.network_self_link -} - -############################################################## -# Inbount DNS Forwarding # -############################################################## - -# TODO Provide resolver addresses in the output once https://github.com/terraform-providers/terraform-provider-google/issues/3753 resolved. -# For now please refer to the documentation on how to get the compute addresses for the DNS Resolver https://cloud.google.com/dns/zones/#creating_a_dns_policy_that_enables_inbound_dns_forwarding -resource "google_dns_policy" "google_dns_policy" { - provider = "google-beta" - - project = var.hub_project_id - name = "inbound-dns-forwarding-policy" - enable_inbound_forwarding = true - - networks { - network_url = module.vpc-hub.network_self_link - } -} diff --git a/infrastructure/hub-and-spoke-vpns/outputs.tf b/infrastructure/hub-and-spoke-vpns/outputs.tf deleted file mode 100644 index 24476f8e2..000000000 --- a/infrastructure/hub-and-spoke-vpns/outputs.tf +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -output "hub" { - description = "Hub network resources." - value = { - network_name = module.vpc-hub.network_name - subnets_ips = zipmap( - module.vpc-hub.subnets_names, - module.vpc-hub.subnets_ips - ) - subnets_regions = zipmap( - module.vpc-hub.subnets_names, - module.vpc-hub.subnets_regions - ) - privte_dns_zone = { - name = module.hub-private-zone.name - domain = module.hub-private-zone.domain - } - forwarding_dns_zone = { - name = module.hub-forwarding-zone.name - domain = module.hub-forwarding-zone.domain - } - } -} - -output "spoke-1" { - description = "Spoke1 network resources." - value = { - network_name = module.vpc-spoke-1.network_name - subnets_ips = zipmap( - module.vpc-spoke-1.subnets_names, - module.vpc-spoke-1.subnets_ips - ) - subnets_regions = zipmap( - module.vpc-spoke-1.subnets_names, - module.vpc-spoke-1.subnets_regions - ) - peering_to_hub_private_dns_zone = { - name = module.spoke-1-peering-zone-to-hub-private-zone.name - domain = module.spoke-1-peering-zone-to-hub-private-zone.domain - } - peering_to_hub_forwarding_dns_zone = { - name = module.spoke-1-peering-zone-to-hub-forwarding-zone.name - domain = module.spoke-1-peering-zone-to-hub-forwarding-zone.domain - } - } -} - -output "spoke-2" { - description = "Spoke2 network resources." - value = { - network_name = module.vpc-spoke-2.network_name - subnets_ips = zipmap( - module.vpc-spoke-2.subnets_names, - module.vpc-spoke-2.subnets_ips - ) - subnets_regions = zipmap( - module.vpc-spoke-2.subnets_names, - module.vpc-spoke-2.subnets_regions - ) - peering_to_hub_private_dns_zone = { - name = module.spoke-2-peering-zone-to-hub-private-zone.name - domain = module.spoke-2-peering-zone-to-hub-private-zone.domain - } - peering_to_hub_forwarding_dns_zone = { - name = module.spoke-2-peering-zone-to-hub-forwarding-zone.name - domain = module.spoke-2-peering-zone-to-hub-forwarding-zone.domain - } - } -} diff --git a/infrastructure/hub-and-spoke-vpns/terraform.tfvars.sample b/infrastructure/hub-and-spoke-vpns/terraform.tfvars.sample deleted file mode 100644 index cfe767c3d..000000000 --- a/infrastructure/hub-and-spoke-vpns/terraform.tfvars.sample +++ /dev/null @@ -1,3 +0,0 @@ -hub_project_id = "automation-examples" -spoke_1_project_id = "automation-examples" -spoke_2_project_id = "automation-examples" diff --git a/infrastructure/hub-and-spoke-vpns/test-resources.tf b/infrastructure/hub-and-spoke-vpns/test-resources.tf deleted file mode 100644 index a49a7bcea..000000000 --- a/infrastructure/hub-and-spoke-vpns/test-resources.tf +++ /dev/null @@ -1,158 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -############################################################################### -# Hub test VMs and DNS records # -############################################################################### - -resource "google_compute_instance" "hub" { - count = length(var.hub_subnets) - project = var.hub_project_id - name = "hub-${element(var.hub_subnets, count.index)["subnet_name"]}" - machine_type = "f1-micro" - zone = "${element(local.hub_subnet_regions, count.index)}-b" - tags = ["ssh"] - boot_disk { - initialize_params { - image = "debian-cloud/debian-9" - } - } - network_interface { - subnetwork = element(module.vpc-hub.subnets_self_links, count.index) - access_config {} - } -} - -resource "google_dns_record_set" "hub" { - count = length(var.hub_subnets) - - project = var.hub_project_id - name = "hub-${count.index}.${module.hub-private-zone.domain}" - type = "A" - ttl = 300 - - managed_zone = module.hub-private-zone.name - - rrdatas = [google_compute_instance.hub[count.index].network_interface.0.network_ip] -} - -############################################################################### -# Spoke 1 test VMs and DNS records # -############################################################################### - -resource "google_compute_instance" "spoke-1" { - count = length(var.spoke_1_subnets) - project = var.spoke_1_project_id - name = "spoke-1-${element(var.spoke_1_subnets, count.index)["subnet_name"]}" - machine_type = "f1-micro" - zone = "${element(local.spoke_1_subnet_regions, count.index)}-b" - tags = ["ssh"] - boot_disk { - initialize_params { - image = "debian-cloud/debian-9" - } - } - network_interface { - subnetwork = element(module.vpc-spoke-1.subnets_self_links, count.index) - access_config {} - } -} - -resource "google_dns_record_set" "spoke-1" { - count = length(var.spoke_1_subnets) - - project = var.hub_project_id - name = "spoke-1-${count.index}.${module.hub-private-zone.domain}" - type = "A" - ttl = 300 - - managed_zone = module.hub-private-zone.name - - rrdatas = [google_compute_instance.spoke-1[count.index].network_interface.0.network_ip] -} - -############################################################################### -# Spoke 2 test VMs and DNS records # -############################################################################### - -resource "google_compute_instance" "spoke-2" { - count = length(var.spoke_2_subnets) - project = var.spoke_2_project_id - name = "spoke-2-${element(var.spoke_2_subnets, count.index)["subnet_name"]}" - machine_type = "f1-micro" - zone = "${element(local.spoke_2_subnet_regions, count.index)}-b" - tags = ["ssh"] - boot_disk { - initialize_params { - image = "debian-cloud/debian-9" - } - } - network_interface { - subnetwork = element(module.vpc-spoke-2.subnets_self_links, count.index) - access_config {} - } -} - -resource "google_dns_record_set" "spoke-2" { - count = length(var.spoke_2_subnets) - - project = var.hub_project_id - name = "spoke-2-${count.index}.${module.hub-private-zone.domain}" - type = "A" - ttl = 300 - - managed_zone = module.hub-private-zone.name - - rrdatas = [google_compute_instance.spoke-2[count.index].network_interface.0.network_ip] -} - -############################################################################### -# test outputs # -############################################################################### - -output "test-instances" { - description = "Test instance attributes." - value = { - hub = { - instance_zones = zipmap( - google_compute_instance.hub.*.name, - google_compute_instance.hub.*.zone - ) - instances_dns_names = zipmap( - google_compute_instance.hub.*.name, - google_dns_record_set.hub.*.name - ) - } - spoke-1 = { - instances_zones = zipmap( - google_compute_instance.spoke-1.*.name, - google_compute_instance.spoke-1.*.zone - ) - instances_dns_names = zipmap( - google_compute_instance.spoke-1.*.name, - google_dns_record_set.spoke-1.*.name - ) - } - spoke-2 = { - instances_zones = zipmap( - google_compute_instance.spoke-2.*.name, - google_compute_instance.spoke-2.*.zone - ) - instances_dns_names = zipmap( - google_compute_instance.spoke-2.*.name, - google_dns_record_set.spoke-2.*.name - ) - } - } -} diff --git a/infrastructure/hub-and-spoke-vpns/variables.tf b/infrastructure/hub-and-spoke-vpns/variables.tf deleted file mode 100644 index a7e69e577..000000000 --- a/infrastructure/hub-and-spoke-vpns/variables.tf +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -variable "hub_project_id" { - description = "Hub Project id. Same project can be used for hub and spokes." - type = string -} - -variable "spoke_1_project_id" { - description = "Spoke 1 Project id. Same project can be used for hub and spokes." - type = string -} - -variable "spoke_2_project_id" { - description = "Spoke 2 Project id. Same project can be used for hub and spokes." - type = string -} - -variable "spoke_to_spoke_route_advertisement" { - description = "Use custom route advertisement in hub routers to advertise all spoke subnets." - type = bool - default = true -} - -variable "hub_bgp_asn" { - description = "Hub BGP ASN." - type = number - default = 64515 -} - -variable "spoke_1_bgp_asn" { - description = "Spoke 1 BGP ASN." - type = number - default = 64516 -} - -variable "spoke_2_bgp_asn" { - description = "Spoke 2 BGP ASN." - type = number - default = 64517 -} - -variable "hub_subnets" { - description = "Hub VPC subnets configuration." - type = list(object({ - subnet_name = string - subnet_ip = string - subnet_region = string - })) - default = [{ - subnet_name = "subnet-a" - subnet_ip = "10.10.10.0/24" - subnet_region = "europe-west1" - }, - { - subnet_name = "subnet-b" - subnet_ip = "10.10.20.0/24" - subnet_region = "europe-west2" - }, - ] -} - -variable "spoke_1_subnets" { - description = "Spoke 1 VPC subnets configuration." - default = [{ - subnet_name = "spoke-1-subnet-a" - subnet_ip = "10.20.10.0/24" - subnet_region = "europe-west1" - }, - { - subnet_name = "spoke-1-subnet-b" - subnet_ip = "10.20.20.0/24" - subnet_region = "europe-west2" - }, - ] -} - -variable "spoke_2_subnets" { - description = "Spoke 2 VPC subnets configuration." - default = [{ - subnet_name = "spoke-2-subnet-a" - subnet_ip = "10.30.10.0/24" - subnet_region = "europe-west1" - }, - { - subnet_name = "spoke-2-subnet-b" - subnet_ip = "10.30.20.0/24" - subnet_region = "europe-west2" - }, - ] -} - -variable "private_dns_zone_name" { - description = "Private DNS Zone Name." - type = string - default = "gcp-local" -} - -variable "private_dns_zone_domain" { - description = "Private DNS Zone Domain." - type = string - default = "gcp.local." -} - -variable "forwarding_dns_zone_name" { - description = "Forwarding DNS Zone Name." - type = string - default = "on-prem-local" -} - -variable "forwarding_dns_zone_domain" { - description = "Forwarding DNS Zone Domain." - type = string - default = "on-prem.local." -} - -variable "forwarding_zone_server_addresses" { - description = "Forwarding DNS Zone Server Addresses" - type = list(string) - default = ["8.8.8.8", "8.8.4.4"] -} diff --git a/infrastructure/onprem-google-access-dns/README.md b/infrastructure/onprem-google-access-dns/README.md new file mode 100644 index 000000000..19ba2b541 --- /dev/null +++ b/infrastructure/onprem-google-access-dns/README.md @@ -0,0 +1,161 @@ +# On-prem DNS and Google Private Access + +This example leverages the [on prem in a box](../../modules/on-prem-in-a-box) module to bootstrap an emulated on-premises environment on GCP, then connects it via VPN and sets up BGP and DNS so that several specific features can be tested: + +- [Cloud DNS forwarding zone](https://cloud.google.com/dns/docs/overview#fz-targets) to on-prem +- DNS forwarding from on-prem via a [Cloud DNS inbound policy](https://cloud.google.com/dns/docs/policies#create-in) +- [Private Access for on-premises hosts](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid) + +The example has been purposefully kept simple to show how to use and wire the on-prem module, but it lends itself well to experimenting and can be combined with the other [infrastructure examples](../) in this repository to test different GCP networking patterns in connection to on-prem. This is the high level diagram: + +![High-level diagram](diagram.png "High-level diagram") + +## Managed resources and services + +This sample creates several distinct groups of resources: + +- one VPC +- one set of firewall rules +- one Cloud NAT configuration +- one test instance +- one service account for the test instance +- one service account for the onprem instance +- one dynamic VPN gateway with a single tunnel +- two DNS zones (private and forwarding) and a DNS inbound policy +- one emulated on-premises environment in a single GCP instance + +## Cloud DNS inbound forwarder entry point + +The Cloud DNS inbound policy reserves an IP address in the VPC, which is used by the on-prem DNS server to forward queries to Cloud DNS. This address needs of course to be explicitly set in the on-prem DNS configuration (see below for details), but since there's currently no way for Terraform to find the exact address (cf [Google provider issue](https://github.com/terraform-providers/terraform-provider-google/issues/3753)), the following manual workaround needs to be applied. + +### Find out the forwarder entry point address + +Run this gcloud command to (find out the address assigned to the inbound forwarder)[https://cloud.google.com/dns/docs/policies#list-in-entrypoints]: + +```bash +gcloud compute addresses list -project [your project id] +``` + +In the list of addresses, look for the address with purpose `DNS_RESOLVER` in the subnet `to-onprem-default`. If its IP address is `10.0.0.2` it matches the default value in the Terraform `forwarder_address` variable, which means you're all set. If it's different, proceed to the next step. + +### Update the forwarder address variable and recreate on-prem + +If the forwader address does not match the Terraform variable, add the correct value in your `terraform.tfvars` (or change the default value in `variables.tf`), then taint the onprem instance and apply to recreate it with the correct value in the DNS configuration: + +```bash +tf apply +tf taint module.on-prem.google_compute_instance.on_prem_in_a_box +tf apply +``` + +## CoreDNS configuration for on-premises + +The on-prem module uses a CoreDNS container to expose its DNS service, configured with foru distinct blocks: + +- the onprem block serving static records for the `onprem.example.com` zone that map to each of the on-prem containers +- the forwarding block for the `gcp.example.com` zone and for Google Private Access, that map to the IP address of the Cloud DNS inbound policy +- the `google.internal` block that exposes to containers a name for the instance metadata address +- the default block that forwards to Google public DNS resolvers + +This is the CoreDNS configuration: + +```coredns +onprem.example.com { + root /etc/coredns + hosts onprem.hosts + log + errors +} +gcp.example.com googleapis.com { + forward . ${resolver_address} + log + errors +} +google.internal { + hosts { + 169.254.169.254 metadata.google.internal + } +} +. { + forward . 8.8.8.8 + log + errors +} +``` + +## Testing + +### Onprem to cloud + +```bash +# connect to the onprem instance +gcloud compute ssh onprem + +# check that the BGP session works and the advertised routes are set +sudo docker exec -it onprem_bird_1 ip route |grep bird +10.0.0.0/24 via 169.254.1.1 dev vti0 proto bird src 10.0.16.2 +35.199.192.0/19 via 169.254.1.1 dev vti0 proto bird src 10.0.16.2 +199.36.153.4/30 via 169.254.1.1 dev vti0 proto bird src 10.0.16.2 + +# get a shell on the toolbox container +sudo docker exec -it onprem_toolbox_1 sh + +# test forwarding from CoreDNS via the Cloud DNS inbound policy +dig test-1.gcp.example.com +short +10.0.0.3 + +# test that Private Access is configured correctly +dig compute.googleapis.com +short +private.googleapis.com. +199.36.153.8 +199.36.153.9 +199.36.153.10 +199.36.153.11 + +# issue an API call via Private Access +gcloud config set project [your project id] +gcloud compute instances list +``` + +### Cloud to onprem + +```bash +# connect to the test instance +gcloud compute ssh test-1 + +# test forwarding from Cloud DNS to onprem CoreDNS (address may differ) +dig gw.onprem.example.com +short +10.0.16.2 + +# test a request to the onprem web server +curl www.onprem.example.com -s |grep h1 +

On Prem in a Box

+``` + +## Operational considerations + +A single pre-existing project is used in this example to keep variables and complexity to a minimum, in a real world scenarios each spoke would probably use a separate project. + +The VPN used to connect to the on-premises environment does not account for HA, upgrading to use HA VPN is reasonably simple by using the relevant [module](../../modules/net-vpn-ha). + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| project_id | Project id for all resources. | string | ✓ | | +| *bgp_asn* | BGP ASNs. | map(number) | | ... | +| *bgp_interface_ranges* | BGP interface IP CIDR ranges. | map(string) | | ... | +| *ip_ranges* | IP CIDR ranges. | map(string) | | ... | +| *region* | VPC region. | string | | europe-west1 | +| *resolver_address* | GCP DNS resolver address for the inbound policy. | string | | 10.0.0.2 | +| *ssh_source_ranges* | IP CIDR ranges that will be allowed to connect via SSH to the onprem instance. | list(string) | | ["0.0.0.0/0"] | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| foo | None | | +| onprem-instance | Onprem instance details. | | +| test-instance | Test instance details. | | + diff --git a/infrastructure/onprem-google-access-dns/assets/Corefile b/infrastructure/onprem-google-access-dns/assets/Corefile new file mode 100644 index 000000000..e5b8432d1 --- /dev/null +++ b/infrastructure/onprem-google-access-dns/assets/Corefile @@ -0,0 +1,21 @@ +onprem.example.com { + root /etc/coredns + hosts onprem.hosts + log + errors +} +gcp.example.com googleapis.com { + forward . ${forwarder_address} + log + errors +} +google.internal { + hosts { + 169.254.169.254 metadata.google.internal + } +} +. { + forward . 8.8.8.8 + log + errors +} diff --git a/infrastructure/onprem-google-access-dns/diagram.png b/infrastructure/onprem-google-access-dns/diagram.png new file mode 100644 index 000000000..3073143d2 Binary files /dev/null and b/infrastructure/onprem-google-access-dns/diagram.png differ diff --git a/infrastructure/onprem-google-access-dns/main.tf b/infrastructure/onprem-google-access-dns/main.tf new file mode 100644 index 000000000..8b73e0049 --- /dev/null +++ b/infrastructure/onprem-google-access-dns/main.tf @@ -0,0 +1,244 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + bgp_interface_gcp = "${cidrhost(var.bgp_interface_ranges.gcp, 1)}" + bgp_interface_onprem = "${cidrhost(var.bgp_interface_ranges.gcp, 2)}" + netblocks = { + dns = data.google_netblock_ip_ranges.dns-forwarders.cidr_blocks_ipv4.0 + api = data.google_netblock_ip_ranges.private-googleapis.cidr_blocks_ipv4.0 + } + vips = { + api = [for i in range(4) : cidrhost(local.netblocks.api, i)] + } + vm-startup-script = join("\n", [ + "#! /bin/bash", + "apt-get update && apt-get install -y bash-completion dnsutils kubectl" + ]) +} + +data "google_netblock_ip_ranges" "private-googleapis" { + range_type = "private-googleapis" +} + +data "google_netblock_ip_ranges" "dns-forwarders" { + range_type = "dns-forwarders" +} + +################################################################################ +# Networking # +################################################################################ + +module "vpc" { + source = "../../modules/net-vpc" + project_id = var.project_id + name = "to-onprem" + subnets = { + default = { + ip_cidr_range = var.ip_ranges.gcp + region = var.region + secondary_ip_range = {} + } + } +} + +module "vpc-firewall" { + source = "../../modules/net-vpc-firewall" + project_id = var.project_id + network = module.vpc.name + admin_ranges_enabled = true + admin_ranges = values(var.ip_ranges) + ssh_source_ranges = var.ssh_source_ranges +} + +module "vpn" { + source = "../../modules/net-vpn-dynamic" + project_id = var.project_id + region = module.vpc.subnet_regions["default"] + network = module.vpc.name + name = "to-onprem" + router_asn = var.bgp_asn.gcp + tunnels = { + onprem = { + bgp_peer = { + address = local.bgp_interface_onprem + asn = var.bgp_asn.onprem + } + bgp_peer_options = { + advertise_groups = ["ALL_SUBNETS"] + advertise_ip_ranges = { + (local.netblocks.api) = "private-googleapis" + (local.netblocks.dns) = "dns-forwarders" + } + advertise_mode = "CUSTOM" + route_priority = 1000 + } + bgp_session_range = "${local.bgp_interface_gcp}/30" + ike_version = 2 + peer_ip = module.on-prem.external_address + shared_secret = "" + } + } +} + +module "nat" { + source = "../../modules/net-cloudnat" + project_id = var.project_id + region = module.vpc.subnet_regions.default + name = "default" + router_create = false + router_name = module.vpn.router_name +} + +################################################################################ +# DNS # +################################################################################ + +module "dns-gcp" { + source = "../../modules/dns" + project_id = var.project_id + type = "private" + name = "gcp-example" + domain = "gcp.example.com." + client_networks = [module.vpc.self_link] + recordsets = concat( + [{ name = "localhost", type = "A", ttl = 300, records = ["127.0.0.1"] }], + [ + for name, ip in zipmap(module.vm-test.names, module.vm-test.internal_ips) : + { name = name, type = "A", ttl = 300, records = [ip] } + ] + ) +} + +module "dns-api" { + source = "../../modules/dns" + project_id = var.project_id + type = "private" + name = "googleapis" + domain = "googleapis.com." + client_networks = [module.vpc.self_link] + recordsets = [ + { + name = "*", type = "CNAME", ttl = 300, records = ["private.googleapis.com."] + }, + { + name = "private", type = "A", ttl = 300, records = local.vips.api + }, + ] +} + +module "dns-onprem" { + source = "../../modules/dns" + project_id = var.project_id + type = "forwarding" + name = "onprem-example" + domain = "onprem.example.com." + client_networks = [module.vpc.self_link] + forwarders = [cidrhost(var.ip_ranges.onprem, 3)] +} + +resource "google_dns_policy" "inbound" { + provider = google-beta + project = var.project_id + name = "gcp-inbound" + enable_inbound_forwarding = true + networks { + network_url = module.vpc.self_link + } +} + +################################################################################ +# Test instance # +################################################################################ + +module "service-account-gce" { + source = "../../modules/iam-service-accounts" + project_id = var.project_id + names = ["gce-test"] + iam_project_roles = { + (var.project_id) = [ + "roles/logging.logWriter", + "roles/monitoring.metricWriter", + ] + } +} + +module "vm-test" { + source = "../../modules/compute-vm" + project_id = var.project_id + region = module.vpc.subnet_regions.default + zone = "${module.vpc.subnet_regions.default}-b" + name = "test" + network_interfaces = [{ + network = module.vpc.self_link, + subnetwork = module.vpc.subnet_self_links.default, + nat = false, + addresses = null + }] + metadata = { startup-script = local.vm-startup-script } + service_account = module.service-account-gce.email + service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"] + tags = ["ssh"] +} + +################################################################################ +# On prem # +################################################################################ + +data "template_file" "corefile" { + template = file("assets/Corefile") + vars = { + forwarder_address = var.forwarder_address + } +} + +module "service-account-onprem" { + source = "../../modules/iam-service-accounts" + project_id = var.project_id + names = ["gce-onprem"] + iam_project_roles = { + (var.project_id) = [ + "roles/compute.viewer", + "roles/logging.logWriter", + "roles/monitoring.metricWriter", + ] + } +} + +module "on-prem" { + source = "../../modules/on-prem-in-a-box/" + project_id = var.project_id + zone = "${var.region}-b" + network = module.vpc.name + subnet_self_link = module.vpc.subnet_self_links.default + local_ip_cidr_range = var.ip_ranges.onprem + coredns_config = data.template_file.corefile.rendered + vpn_config = { + peer_ip = module.vpn.address + shared_secret = module.vpn.random_secret + type = "dynamic" + } + vpn_dynamic_config = { + local_bgp_asn = var.bgp_asn.onprem + local_bgp_address = local.bgp_interface_onprem + peer_bgp_asn = var.bgp_asn.gcp + peer_bgp_address = local.bgp_interface_gcp + } + service_account = { + email = module.service-account-onprem.email + scopes = ["https://www.googleapis.com/auth/cloud-platform"] + } +} diff --git a/infrastructure/onprem-google-access-dns/outputs.tf b/infrastructure/onprem-google-access-dns/outputs.tf new file mode 100644 index 000000000..559bec4ce --- /dev/null +++ b/infrastructure/onprem-google-access-dns/outputs.tf @@ -0,0 +1,39 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "onprem-instance" { + description = "Onprem instance details." + value = join(" ", [ + module.on-prem.instance_name, + module.on-prem.internal_address, + module.on-prem.external_address + ]) +} + +output "test-instance" { + description = "Test instance details." + value = join(" ", [ + module.vm-test.names[0], + module.vm-test.internal_ips[0] + ]) +} + +output "foo" { + value = { + dns = data.google_netblock_ip_ranges.dns-forwarders.cidr_blocks_ipv4 + apis = data.google_netblock_ip_ranges.private-googleapis.cidr_blocks_ipv4 + } +} diff --git a/infrastructure/onprem-google-access-dns/variables.tf b/infrastructure/onprem-google-access-dns/variables.tf new file mode 100644 index 000000000..907bedb33 --- /dev/null +++ b/infrastructure/onprem-google-access-dns/variables.tf @@ -0,0 +1,64 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "bgp_asn" { + description = "BGP ASNs." + type = map(number) + default = { + gcp = 64513 + onprem = 64514 + } +} + +variable "bgp_interface_ranges" { + description = "BGP interface IP CIDR ranges." + type = map(string) + default = { + gcp = "169.254.1.0/30" + } +} + +variable "ip_ranges" { + description = "IP CIDR ranges." + type = map(string) + default = { + gcp = "10.0.0.0/24" + onprem = "10.0.16.0/24" + } +} + +variable "project_id" { + description = "Project id for all resources." + type = string +} + +variable "region" { + description = "VPC region." + type = string + default = "europe-west1" +} + +variable "forwarder_address" { + description = "GCP DNS inbound policy forwarder address." + type = string + default = "10.0.0.2" +} + +variable "ssh_source_ranges" { + description = "IP CIDR ranges that will be allowed to connect via SSH to the onprem instance." + type = list(string) + default = ["0.0.0.0/0"] +} diff --git a/infrastructure/shared-vpc/versions.tf b/infrastructure/onprem-google-access-dns/versions.tf similarity index 95% rename from infrastructure/shared-vpc/versions.tf rename to infrastructure/onprem-google-access-dns/versions.tf index 4eb1500c5..de5425c28 100644 --- a/infrastructure/shared-vpc/versions.tf +++ b/infrastructure/onprem-google-access-dns/versions.tf @@ -1,4 +1,4 @@ -# Copyright 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/infrastructure/shared-vpc-gke/README.md b/infrastructure/shared-vpc-gke/README.md new file mode 100644 index 000000000..1ddeb39ac --- /dev/null +++ b/infrastructure/shared-vpc-gke/README.md @@ -0,0 +1,73 @@ +# Shared VPC with GKE example + +This sample creates a basic [Shared VPC](https://cloud.google.com/vpc/docs/shared-vpc) setup using one host project and two service projects, each with a specific subnet in the shared VPC. The setup also includes the specific IAM-level configurations needed for [GKE on Shared VPC](https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-shared-vpc) to enable cluster creation in one of the two service projects. + +The sample has been purposefully kept simple so that it can be used as a basis for different Shared VPC configurations. This is the high level diagram: + +![High-level diagram](diagram.png "High-level diagram") + +## Managed resources and services + +This sample creates several distinct groups of resources: + +- projects + - host project + - service project configured for GKE clusters + - service project configured for GCE instances +- networking + - the shared VPC network + - one subnet with secondary ranges for GKE clusters + - one subnet for GCE instances + - firewall rules for [SSH access via IAP](https://cloud.google.com/iap/docs/using-tcp-forwarding) and open communication within the VPC + - Cloud NAT service +- IAM + - one service account for the bastion CGE instance + - one service account for the GKE nodes + - optional owner role bindings on each project + - optional [OS Login](https://cloud.google.com/compute/docs/oslogin/) role bindings on the GCE service project + - role bindings to allow the GCE instance and GKE nodes logging and monitoring write access + - role binding to allow the GCE instance cluster access +- DNS + - one private zone +- GCE + - one instance used to access the internal GKE cluster +- GKE + - one private cluster with one nodepool + +## Accessing the bastion instance and GKE cluster + +The bastion VM has no public address so access is mediated via [IAP](https://cloud.google.com/iap/docs), which is supported transparently in the `gcloud compute ssh` command. Authentication is via OS Login set as a project default. + +Cluster access from the bastion can leverage the instance service account's `container.developer` role: the only configuration needed is to fetch cluster credentials via `gcloud container clusters get-credentials` passing the correct cluster name, location and project via command options. + +## Destroying + +There's a minor glitch that can surface running `terraform destroy`, where the service project attachments to the Shared VPC will not get destroyed even with the relevant API call succeeding. We are investigating the issue, in the meantime just manually remove the attachment in the Cloud console or via the `gcloud beta compute shared-vpc associated-projects remove` command when `terraform destroy` fails, and then relaunch the command. + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| billing_account_id | Billing account id used as default for new projects. | string | ✓ | | +| prefix | Prefix used for resources that need unique names. | string | ✓ | | +| root_node | Hierarchy node where projects will be created, 'organizations/org_id' or 'folders/folder_id'. | string | ✓ | | +| *ip_ranges* | Subnet IP CIDR ranges. | map(string) | | ... | +| *ip_secondary_ranges* | Secondary IP CIDR ranges. | map(string) | | ... | +| *owners_gce* | GCE project owners, in IAM format. | list(string) | | [] | +| *owners_gke* | GKE project owners, in IAM format. | list(string) | | [] | +| *owners_host* | Host project owners, in IAM format. | list(string) | | [] | +| *private_service_ranges* | Private service IP CIDR ranges. | map(string) | | ... | +| *project_services* | Service APIs enabled by default in new projects. | list(string) | | ... | +| *region* | Region used. | string | | europe-west1 | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| gke_clusters | GKE clusters information. | | +| projects | Project ids. | | +| service_accounts | GCE and GKE service accounts. | | +| vms | GCE VMs. | | +| vpc | Shared VPC. | | + diff --git a/infrastructure/shared-vpc/backend.tf.sample b/infrastructure/shared-vpc-gke/backend.tf.sample similarity index 100% rename from infrastructure/shared-vpc/backend.tf.sample rename to infrastructure/shared-vpc-gke/backend.tf.sample diff --git a/infrastructure/shared-vpc/diagram.gcpdraw b/infrastructure/shared-vpc-gke/diagram.gcpdraw similarity index 72% rename from infrastructure/shared-vpc/diagram.gcpdraw rename to infrastructure/shared-vpc-gke/diagram.gcpdraw index 9ed42b4f6..ff8291e67 100644 --- a/infrastructure/shared-vpc/diagram.gcpdraw +++ b/infrastructure/shared-vpc-gke/diagram.gcpdraw @@ -7,16 +7,17 @@ elements { group host_services { name "Shared Services" background_color "#f6f6f6" - card dns - card kms + card dns { + name "Private zone" + } + card nat { + name "NAT" + } } group vpc_host { name "Shared VPC" background_color "#fff3e0" - card vpc as net_subnet { - name "Networking subnet" - } card vpc as gce_subnet { name "GCE subnet" } @@ -28,15 +29,15 @@ elements { group project_gce { name "GCE service project" - stacked_card gce as gce_instances { - name "VM instances" + card gce as gce_instances { + name "Bastion VM" } } group project_gke { name "GKE service project" - stacked_card gke as gke_clusters { - name "GKE clusters" + card gke as gke_clusters { + name "GKE cluster" } } @@ -46,4 +47,4 @@ elements { paths { gce_subnet ..> gce_instances gke_subnet ..> gke_clusters -} \ No newline at end of file +} diff --git a/infrastructure/shared-vpc-gke/diagram.png b/infrastructure/shared-vpc-gke/diagram.png new file mode 100644 index 000000000..8dc15e9b2 Binary files /dev/null and b/infrastructure/shared-vpc-gke/diagram.png differ diff --git a/infrastructure/shared-vpc-gke/main.tf b/infrastructure/shared-vpc-gke/main.tf new file mode 100644 index 000000000..7c47ad812 --- /dev/null +++ b/infrastructure/shared-vpc-gke/main.tf @@ -0,0 +1,249 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.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. + +############################################################################### +# Host and service projects # +############################################################################### + +# the container.hostServiceAgentUser role is needed for GKE on shared VPC + +module "project-host" { + source = "../../modules/project" + parent = var.root_node + billing_account = var.billing_account_id + prefix = var.prefix + name = "net" + services = concat(var.project_services, ["dns.googleapis.com"]) + iam_roles = [ + "roles/container.hostServiceAgentUser", "roles/owner" + ] + iam_members = { + "roles/container.hostServiceAgentUser" = [ + "serviceAccount:${module.project-svc-gke.gke_service_account}" + ] + "roles/owner" = var.owners_host + } +} + +module "project-svc-gce" { + source = "../../modules/project" + parent = var.root_node + billing_account = var.billing_account_id + prefix = var.prefix + name = "gce" + services = var.project_services + oslogin = true + oslogin_admins = var.owners_gce + iam_roles = [ + "roles/logging.logWriter", + "roles/monitoring.metricWriter", + "roles/owner" + ] + iam_members = { + "roles/logging.logWriter" = [module.vm-bastion.service_account_iam_email], + "roles/monitoring.metricWriter" = [module.vm-bastion.service_account_iam_email], + "roles/owner" = var.owners_gce, + } +} + +# the container.developer role assigned to the bastion instance service account +# allows to fetch GKE credentials from bastion for clusters in this project + +module "project-svc-gke" { + source = "../../modules/project" + parent = var.root_node + billing_account = var.billing_account_id + prefix = var.prefix + name = "gke" + services = var.project_services + iam_roles = [ + "roles/container.developer", + "roles/owner", + ] + iam_members = { + "roles/owner" = var.owners_gke + "roles/container.developer" = [module.vm-bastion.service_account_iam_email] + } +} + +################################################################################ +# Networking # +################################################################################ + +# the service project GKE robot needs the `hostServiceAgent` role throughout +# the entire life of its clusters; the `iam_project_id` project output is used +# here to set the project id so that the VPC depends on that binding, and any +# cluster using it then also depends on it indirectly; you can of course use +# the `project_id` output instead if you don't care about destroying + +# subnet IAM bindings control which identities can use the individual subnets + +module "vpc-shared" { + source = "../../modules/net-vpc" + project_id = module.project-host.iam_project_id + name = "shared-vpc" + shared_vpc_host = true + shared_vpc_service_projects = [ + module.project-svc-gce.project_id, + module.project-svc-gke.project_id + ] + subnets = { + gce = { + ip_cidr_range = var.ip_ranges.gce + region = var.region + secondary_ip_range = {} + } + gke = { + ip_cidr_range = var.ip_ranges.gke + region = var.region + secondary_ip_range = { + pods = var.ip_secondary_ranges.gke-pods + services = var.ip_secondary_ranges.gke-services + } + } + } + iam_roles = { + gke = ["roles/compute.networkUser", "roles/compute.securityAdmin"] + gce = ["roles/compute.networkUser"] + } + iam_members = { + gce = { + "roles/compute.networkUser" = concat(var.owners_gce, [ + "serviceAccount:${module.project-svc-gce.cloudsvc_service_account}", + ]) + } + gke = { + "roles/compute.networkUser" = concat(var.owners_gke, [ + "serviceAccount:${module.project-svc-gke.cloudsvc_service_account}", + "serviceAccount:${module.project-svc-gke.gke_service_account}", + ]) + "roles/compute.securityAdmin" = [ + "serviceAccount:${module.project-svc-gke.gke_service_account}", + ] + } + } +} + +module "vpc-shared-firewall" { + source = "../../modules/net-vpc-firewall" + project_id = module.project-host.project_id + network = module.vpc-shared.name + admin_ranges_enabled = true + admin_ranges = values(var.ip_ranges) +} + +module "nat" { + source = "../../modules/net-cloudnat" + project_id = module.project-host.project_id + region = var.region + name = "vpc-shared" + router_create = true + router_network = module.vpc-shared.name +} + +################################################################################ +# DNS # +################################################################################ + +module "host-dns" { + source = "../../modules/dns" + project_id = module.project-host.project_id + type = "private" + name = "example" + domain = "example.com." + client_networks = [module.vpc-shared.self_link] + recordsets = [ + { name = "localhost", type = "A", ttl = 300, records = ["127.0.0.1"] }, + { name = "bastion", type = "A", ttl = 300, records = module.vm-bastion.internal_ips }, + ] +} + +################################################################################ +# VM # +################################################################################ + +module "vm-bastion" { + source = "../../modules/compute-vm" + project_id = module.project-svc-gce.project_id + region = module.vpc-shared.subnet_regions.gce + zone = "${module.vpc-shared.subnet_regions.gce}-b" + name = "bastion" + network_interfaces = [{ + network = module.vpc-shared.self_link, + subnetwork = lookup(module.vpc-shared.subnet_self_links, "gce", null), + nat = false, + addresses = null + }] + instance_count = 1 + tags = ["ssh"] + metadata = { + startup-script = join("\n", [ + "#! /bin/bash", + "apt-get update", + "apt-get install -y bash-completion kubectl dnsutils" + ]) + } + service_account_create = true +} + +################################################################################ +# GKE # +################################################################################ + +module "cluster-1" { + source = "../../modules/gke-cluster" + name = "cluster-1" + project_id = module.project-svc-gke.project_id + location = "${module.vpc-shared.subnet_regions.gke}-b" + network = module.vpc-shared.self_link + subnetwork = module.vpc-shared.subnet_self_links.gke + secondary_range_pods = "pods" + secondary_range_services = "services" + default_max_pods_per_node = 32 + labels = { + environment = "test" + } + master_authorized_ranges = { + internal-vms = var.ip_ranges.gce + } + private_cluster_config = { + enable_private_nodes = true + enable_private_endpoint = true + master_ipv4_cidr_block = var.private_service_ranges.cluster-1 + } +} + +module "cluster-1-nodepool-1" { + source = "../../modules/gke-nodepool" + name = "nodepool-1" + project_id = module.project-svc-gke.project_id + location = module.cluster-1.location + cluster_name = module.cluster-1.name + node_config_service_account = module.service-account-gke-node.email +} + +# roles assigned via this module use non-authoritative IAM bindings at the +# project level, with no risk of conflicts with pre-existing roles + +module "service-account-gke-node" { + source = "../../modules/iam-service-accounts" + project_id = module.project-svc-gke.project_id + names = ["gke-node"] + iam_project_roles = { + (module.project-svc-gke.project_id) = [ + "roles/logging.logWriter", + "roles/monitoring.metricWriter", + ] + } +} diff --git a/infrastructure/shared-vpc-gke/outputs.tf b/infrastructure/shared-vpc-gke/outputs.tf new file mode 100644 index 000000000..fccebe77e --- /dev/null +++ b/infrastructure/shared-vpc-gke/outputs.tf @@ -0,0 +1,54 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +output "gke_clusters" { + description = "GKE clusters information." + value = { + cluster-1 = module.cluster-1.endpoint + } +} + +output "projects" { + description = "Project ids." + value = { + host = module.project-host.project_id + service-gce = module.project-svc-gce.project_id + service-gke = module.project-svc-gke.project_id + } +} + +output "service_accounts" { + description = "GCE and GKE service accounts." + value = { + bastion = module.vm-bastion.service_account_email + gke_node = module.service-account-gke-node.email + } +} + +output "vpc" { + description = "Shared VPC." + value = { + name = module.vpc-shared.name + subnets = module.vpc-shared.subnet_ips + } +} + +output "vms" { + description = "GCE VMs." + value = { + for instance in concat(module.vm-bastion.instances) : + instance.name => instance.network_interface.0.network_ip + } +} + diff --git a/infrastructure/shared-vpc-gke/variables.tf b/infrastructure/shared-vpc-gke/variables.tf new file mode 100644 index 000000000..a69be5b6a --- /dev/null +++ b/infrastructure/shared-vpc-gke/variables.tf @@ -0,0 +1,87 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +variable "billing_account_id" { + description = "Billing account id used as default for new projects." + type = string +} + +variable "owners_gce" { + description = "GCE project owners, in IAM format." + type = list(string) + default = [] +} + +variable "owners_gke" { + description = "GKE project owners, in IAM format." + type = list(string) + default = [] +} + +variable "owners_host" { + description = "Host project owners, in IAM format." + type = list(string) + default = [] +} + +variable "prefix" { + description = "Prefix used for resources that need unique names." + type = string +} + +variable "region" { + description = "Region used." + type = string + default = "europe-west1" +} + +variable "root_node" { + description = "Hierarchy node where projects will be created, 'organizations/org_id' or 'folders/folder_id'." + type = string +} + +variable "ip_ranges" { + description = "Subnet IP CIDR ranges." + type = map(string) + default = { + gce = "10.0.16.0/24" + gke = "10.0.32.0/24" + } +} + +variable "ip_secondary_ranges" { + description = "Secondary IP CIDR ranges." + type = map(string) + default = { + gke-pods = "10.128.0.0/18" + gke-services = "172.16.0.0/24" + } +} + +variable "private_service_ranges" { + description = "Private service IP CIDR ranges." + type = map(string) + default = { + cluster-1 = "192.168.0.0/28" + } +} + +variable "project_services" { + description = "Service APIs enabled by default in new projects." + type = list(string) + default = [ + "resourceviews.googleapis.com", + "stackdriver.googleapis.com", + ] +} diff --git a/foundations/business-units/modules/business-unit-folders/versions.tf b/infrastructure/shared-vpc-gke/versions.tf similarity index 100% rename from foundations/business-units/modules/business-unit-folders/versions.tf rename to infrastructure/shared-vpc-gke/versions.tf diff --git a/infrastructure/shared-vpc/.terraform.tfstate.lock.info b/infrastructure/shared-vpc/.terraform.tfstate.lock.info deleted file mode 100644 index b77041555..000000000 --- a/infrastructure/shared-vpc/.terraform.tfstate.lock.info +++ /dev/null @@ -1 +0,0 @@ -{"ID":"0de052d8-e6ee-a792-20d2-9cafff96bd13","Operation":"OperationTypeApply","Info":"","Who":"ludomagno@lancre","Version":"0.12.16","Created":"2020-02-18T19:52:11.578559433Z","Path":"terraform.tfstate"} \ No newline at end of file diff --git a/infrastructure/shared-vpc/README.md b/infrastructure/shared-vpc/README.md deleted file mode 100644 index 9bec065a6..000000000 --- a/infrastructure/shared-vpc/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Shared VPC sample - -This sample creates a basic [Shared VPC](https://cloud.google.com/vpc/docs/shared-vpc) infrastructure, where two service projects are connected to separate subnets, and the host project exposes Cloud DNS and Cloud KMS as centralized services. The service projects are slightly different, as they are meant to illustrate the IAM-level differences that need to be taken into account when sharing subnets for GCE or GKE. - -The purpose of this sample is showing how to wire different [Cloud Foundation Fabric](https://github.com/search?q=topic%3Acft-fabric+org%3Aterraform-google-modules&type=Repositories) modules to create Shared VPC infrastructures, and as such it is meant to be used for prototyping, or to experiment with networking configurations. Additional best practices and security considerations need to be taken into account for real world usage (eg removal of default service accounts, disabling of external IPs, firewall design, etc). - -![High-level diagram](diagram.png "High-level diagram") - -## Managed resources and services - -This sample creates several distinct groups of resources: - -- three projects (Shared VPC host and two service projects) -- VPC-level resources (VPC, subnets, firewall rules, etc.) in the host project -- one internal Cloud DNS zone in the host project -- one Cloud KMS keyring with one key in the host project -- IAM roles to wire all the above resource together -- one test instance in each project, with their associated DNS records - -## Test resources - -A set of test resources are included for convenience, as they facilitate experimenting with different networking configurations (firewall rules, external connectivity via VPN, etc.). They are encapsulated in the `test-resources.tf` file, and can be safely removed as a single unit. - -SSH access to instances is configured via [OS Login](https://cloud.google.com/compute/docs/oslogin/), except for the GKE project instance since [GKE nodes do not support OS Login](https://cloud.google.com/compute/docs/instances/managing-instance-access#limitations). To access the GKe instance, use a SSH key set at the project or instance level. External access is allowed via the default SSH rule created by the firewall module, and corresponding `ssh` tags on the instances. - -The GCE instance is somewhat special, as it's configured to run a containerized MySQL server using the [`cos-mysql` module](https://github.com/terraform-google-modules/terraform-google-container-vm/tree/master/modules/cos-mysql), to show a practical example of using this module with KMS encryption for its secret, and to demonstrate how to define a custom firewall rule in the firewall module. - -The networking and GKE instances have `dig` and the `mysql` client installed via startup scripts, so that tests can be run as soon as they are created. - -## Destroying - -There's a minor glitch that can surface running `terraform destroy`, with a simple workaround. The glitch is due to a delay between the API reporting service project removal from the Shared VPC as successful (`google_compute_shared_vpc_service_project` resources destroyed), and the Shared VPC resource being aligned with that event. This results in an error that prevents disabling the Shared VPC feature: `Error disabling Shared VPC Host [...] Cannot disable project as a shared VPC host because it has active service projects.`. The workaround is to run `terraform destroy` again after a few seconds, giving the Shared VPC resource time to be in sync with service project removal. - - -## Variables - -| name | description | type | required | default | -|---|---|:---: |:---:|:---:| -| billing_account_id | Billing account id used as default for new projects. | string | ✓ | | -| prefix | Prefix used for resources that need unique names. | string | ✓ | | -| root_node | Hierarchy node where projects will be created, 'organizations/org_id' or 'folders/folder_id'. | string | ✓ | | -| *kms_keyring_location* | Location used for the KMS keyring. | string | | europe | -| *kms_keyring_name* | Name used for the KMS keyring. | string | | svpc-example | -| *oslogin_admins_gce* | GCE project oslogin admin members, in IAM format. | list(string) | | [] | -| *oslogin_users_gce* | GCE project oslogin user members, in IAM format. | list(string) | | [] | -| *owners_gce* | GCE project owners, in IAM format. | list(string) | | [] | -| *owners_gke* | GKE project owners, in IAM format. | list(string) | | [] | -| *owners_host* | Host project owners, in IAM format. | list(string) | | [] | -| *project_services* | Service APIs enabled by default in new projects. | list(string) | | ... | -| *subnet_secondary_ranges* | Shared VPC subnets secondary range definitions. | map(list(object({...}))) | | ... | -| *subnets* | Shared VPC subnet definitions. | list(object({...})) | | ... | - -## Outputs - -| name | description | sensitive | -|---|---|:---:| -| host_project_id | VPC host project id. | | -| service_project_ids | Service project ids. | | -| vpc_name | Shared VPC name | | -| vpc_subnets | Shared VPC subnets. | | - diff --git a/infrastructure/shared-vpc/diagram.png b/infrastructure/shared-vpc/diagram.png deleted file mode 100644 index 038679540..000000000 Binary files a/infrastructure/shared-vpc/diagram.png and /dev/null differ diff --git a/infrastructure/shared-vpc/locals.tf b/infrastructure/shared-vpc/locals.tf deleted file mode 100644 index bf4612249..000000000 --- a/infrastructure/shared-vpc/locals.tf +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -locals { - # GCE service project users that need the network user role assigned on host - net_gce_users = concat( - var.owners_gce, - ["serviceAccount:${module.project-service-gce.cloudsvc_service_account}"] - ) - # GKE service project users that need the network user role assigned on host - net_gke_users = concat( - var.owners_gke, - [ - "serviceAccount:${module.project-service-gke.gke_service_account}", - "serviceAccount:${module.project-service-gke.cloudsvc_service_account}" - ] - ) - # GKE subnet primary and secondary ranges, used in firewall rules - # use lookup to prevent failure on successive destroys - net_gke_ip_ranges = compact([ - lookup(local.net_subnet_ips, "gke", ""), - element([ - for range in lookup(var.subnet_secondary_ranges, "gke", []) : - range.ip_cidr_range if range.range_name == "pods" - ], 0) - ]) - # map of subnet names => addresses - net_subnet_ips = zipmap( - module.net-vpc-host.subnets_names, - module.net-vpc-host.subnets_ips - ) - # map of subnet names => links - net_subnet_links = zipmap( - module.net-vpc-host.subnets_names, - module.net-vpc-host.subnets_self_links - ) - # map of subnet names => regions - net_subnet_regions = zipmap( - module.net-vpc-host.subnets_names, - module.net-vpc-host.subnets_regions - ) - # use svpc access module outputs to create an implicit dependency on service project registration - service_projects = zipmap( - module.net-svpc-access.service_projects, - module.net-svpc-access.service_projects - ) -} diff --git a/infrastructure/shared-vpc/main.tf b/infrastructure/shared-vpc/main.tf deleted file mode 100644 index 8b55e9f03..000000000 --- a/infrastructure/shared-vpc/main.tf +++ /dev/null @@ -1,160 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -############################################################################### -# Host and service projects # -############################################################################### - -# host project - -module "project-svpc-host" { - source = "terraform-google-modules/project-factory/google//modules/fabric-project" - version = "7.0.0" - parent = var.root_node - prefix = var.prefix - name = "vpc-host" - billing_account = var.billing_account_id - owners = var.owners_host - activate_apis = concat( - var.project_services, - ["dns.googleapis.com", "cloudkms.googleapis.com"] - ) -} - -# service projects - -module "project-service-gce" { - source = "terraform-google-modules/project-factory/google//modules/fabric-project" - version = "7.0.0" - parent = var.root_node - prefix = var.prefix - name = "gce" - billing_account = var.billing_account_id - oslogin = "true" - owners = var.owners_gce - oslogin_admins = var.oslogin_admins_gce - oslogin_users = var.oslogin_users_gce - activate_apis = var.project_services -} - -module "project-service-gke" { - source = "terraform-google-modules/project-factory/google//modules/fabric-project" - version = "7.0.0" - parent = var.root_node - prefix = var.prefix - name = "gke" - billing_account = var.billing_account_id - owners = var.owners_gke - activate_apis = var.project_services -} - -################################################################################ -# Networking # -################################################################################ - -# Shared VPC - -module "net-vpc-host" { - source = "terraform-google-modules/network/google" - version = "2.1.1" - project_id = module.project-svpc-host.project_id - network_name = "vpc-shared" - shared_vpc_host = true - subnets = var.subnets - secondary_ranges = var.subnet_secondary_ranges - routes = [] -} - -# Shared VPC firewall - -module "net-vpc-firewall" { - source = "terraform-google-modules/network/google//modules/fabric-net-firewall" - version = "2.1.1" - project_id = module.project-svpc-host.project_id - network = module.net-vpc-host.network_name - admin_ranges_enabled = true - admin_ranges = compact([lookup(local.net_subnet_ips, "networking", "")]) - custom_rules = { - ingress-mysql = { - description = "Allow incoming connections on the MySQL port from GKE addresses." - direction = "INGRESS" - action = "allow" - ranges = local.net_gke_ip_ranges - sources = [] - targets = ["mysql"] - use_service_accounts = false - rules = [{ protocol = "tcp", ports = [3306] }] - extra_attributes = {} - } - } -} - -# Shared VPC access - -module "net-svpc-access" { - source = "terraform-google-modules/network/google//modules/fabric-net-svpc-access" - version = "2.1.1" - host_project_id = module.project-svpc-host.project_id - service_project_num = 2 - service_project_ids = [ - module.project-service-gce.project_id, - module.project-service-gke.project_id - ] - host_subnets = ["gce", "gke"] - host_subnet_regions = compact([ - lookup(local.net_subnet_regions, "gce", ""), - lookup(local.net_subnet_regions, "gke", "") - ]) - host_subnet_users = { - gce = join(",", local.net_gce_users) - gke = join(",", local.net_gke_users) - } - host_service_agent_role = true - host_service_agent_users = [ - "serviceAccount:${module.project-service-gke.gke_service_account}" - ] -} - -################################################################################ -# DNS # -################################################################################ - -module "host-dns" { - source = "terraform-google-modules/cloud-dns/google" - version = "3.0.0" - project_id = module.project-svpc-host.project_id - type = "private" - name = "svpc-fabric-example" - domain = "svpc.fabric." - private_visibility_config_networks = [module.net-vpc-host.network_self_link] - recordsets = [ - { name = "localhost", type = "A", ttl = 300, records = ["127.0.0.1"] } - ] -} - -################################################################################ -# KMS # -################################################################################ - -module "host-kms" { - source = "terraform-google-modules/kms/google" - version = "1.1.0" - project_id = module.project-svpc-host.project_id - location = var.kms_keyring_location - keyring = var.kms_keyring_name - keys = ["mysql"] - set_decrypters_for = ["mysql"] - decrypters = ["serviceAccount:${module.project-service-gce.gce_service_account}"] - prevent_destroy = false -} diff --git a/infrastructure/shared-vpc/outputs.tf b/infrastructure/shared-vpc/outputs.tf deleted file mode 100644 index c7e021d07..000000000 --- a/infrastructure/shared-vpc/outputs.tf +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -output "vpc_name" { - description = "Shared VPC name" - value = module.net-vpc-host.network_name -} - -output "vpc_subnets" { - description = "Shared VPC subnets." - value = local.net_subnet_ips -} - -output "host_project_id" { - description = "VPC host project id." - value = module.project-svpc-host.project_id -} - -output "service_project_ids" { - description = "Service project ids." - value = { - gce = module.project-service-gce.project_id - gke = module.project-service-gke.project_id - } -} diff --git a/infrastructure/shared-vpc/test-resources.tf b/infrastructure/shared-vpc/test-resources.tf deleted file mode 100644 index b24ce58c7..000000000 --- a/infrastructure/shared-vpc/test-resources.tf +++ /dev/null @@ -1,167 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -############################################################################### -# host test VM and DNS record # -############################################################################### - -resource "google_compute_instance" "test-net" { - project = module.project-svpc-host.project_id - name = "test-net" - machine_type = "f1-micro" - zone = "${local.net_subnet_regions.networking}-b" - tags = ["ssh"] - boot_disk { - initialize_params { - image = "debian-cloud/debian-9" - } - } - network_interface { - network = module.net-vpc-host.network_self_link - subnetwork = local.net_subnet_links.networking - access_config {} - } - metadata_startup_script = "apt update && apt install -y dnsutils mysql-client" -} - -resource "google_dns_record_set" "test_net" { - project = module.project-svpc-host.project_id - name = "test-net.${module.host-dns.domain}" - type = "A" - ttl = 300 - managed_zone = module.host-dns.name - rrdatas = [ - google_compute_instance.test-net.network_interface.0.network_ip - ] -} - -############################################################################### -# GKE project test VM and DNS record # -############################################################################### - -resource "google_compute_instance" "test-gke" { - depends_on = [module.net-svpc-access] - project = module.project-service-gke.project_id - name = "test-gke" - machine_type = "f1-micro" - zone = "${local.net_subnet_regions.gke}-b" - tags = ["ssh"] - boot_disk { - initialize_params { - image = "debian-cloud/debian-9" - } - } - network_interface { - network = module.net-vpc-host.network_self_link - subnetwork = local.net_subnet_links.gke - access_config {} - } - metadata_startup_script = "apt update && apt install -y dnsutils mysql-client" -} - -resource "google_dns_record_set" "test_gke" { - project = module.project-svpc-host.project_id - name = "test-gke.${module.host-dns.domain}" - type = "A" - ttl = 300 - managed_zone = module.host-dns.name - rrdatas = [ - google_compute_instance.test-gke.network_interface.0.network_ip - ] -} - -############################################################################### -# GCE project MySQL test VM and DNS record # -############################################################################### - -# random password for MySQL - -resource "random_pet" "mysql_password" {} - -# MySQL password encrypted via KMS key - -data "google_kms_secret_ciphertext" "mysql_password" { - crypto_key = module.host-kms.keys.mysql - plaintext = random_pet.mysql_password.id -} - -# work around the encrypted password always refreshing, taint to refresh - -resource "null_resource" "mysql_password" { - triggers = { - ciphertext = data.google_kms_secret_ciphertext.mysql_password.ciphertext - } - lifecycle { - ignore_changes = [triggers] - } -} - -# MySQL container on Container Optimized OS - -module "container-vm_cos-mysql" { - source = "terraform-google-modules/container-vm/google//modules/cos-mysql" - version = "1.0.4" - project_id = lookup(local.service_projects, module.project-service-gce.project_id, "") - region = "${lookup(local.net_subnet_regions, "gce", "")}" - zone = "${lookup(local.net_subnet_regions, "gce", "")}-b" - network = module.net-vpc-host.network_self_link - subnetwork = lookup(local.net_subnet_links, "gke", "") - instance_count = "1" - data_disk_size = "10" - vm_tags = ["ssh", "mysql"] - password = null_resource.mysql_password.triggers.ciphertext - # TODO(ludomagno): add a location output to the keyring module - kms_data = { - key = "mysql" - keyring = module.host-kms.keyring_name - location = var.kms_keyring_location - project_id = module.project-svpc-host.project_id - } -} - -resource "google_dns_record_set" "mysql" { - project = module.project-svpc-host.project_id - name = "mysql.${module.host-dns.domain}" - type = "A" - ttl = 300 - managed_zone = module.host-dns.name - rrdatas = [ - values(module.container-vm_cos-mysql.instances)[0] - ] -} - -############################################################################### -# test outputs # -############################################################################### - -output "test-instances" { - description = "Test instance names." - value = { - gke = map( - google_compute_instance.test-gke.name, - google_compute_instance.test-gke.network_interface.0.network_ip - ) - mysql = module.container-vm_cos-mysql.instances - networking = map( - google_compute_instance.test-net.name, - google_compute_instance.test-net.network_interface.0.network_ip - ) - } -} - -output "mysql-root-password" { - description = "Password for the test MySQL db root user." - sensitive = true - value = random_pet.mysql_password.id -} diff --git a/infrastructure/shared-vpc/variables.tf b/infrastructure/shared-vpc/variables.tf deleted file mode 100644 index 0f1f03fa3..000000000 --- a/infrastructure/shared-vpc/variables.tf +++ /dev/null @@ -1,131 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -variable "billing_account_id" { - description = "Billing account id used as default for new projects." - type = string -} - -variable "kms_keyring_location" { - description = "Location used for the KMS keyring." - type = string - default = "europe" -} - -variable "kms_keyring_name" { - description = "Name used for the KMS keyring." - type = string - default = "svpc-example" -} - -variable "oslogin_admins_gce" { - description = "GCE project oslogin admin members, in IAM format." - type = list(string) - default = [] -} - -variable "oslogin_users_gce" { - description = "GCE project oslogin user members, in IAM format." - type = list(string) - default = [] -} - -variable "owners_gce" { - description = "GCE project owners, in IAM format." - type = list(string) - default = [] -} - -variable "owners_gke" { - description = "GKE project owners, in IAM format." - type = list(string) - default = [] -} - -variable "owners_host" { - description = "Host project owners, in IAM format." - type = list(string) - default = [] -} - -variable "prefix" { - description = "Prefix used for resources that need unique names." - type = string -} - -variable "root_node" { - description = "Hierarchy node where projects will be created, 'organizations/org_id' or 'folders/folder_id'." - type = string -} - -variable "subnets" { - description = "Shared VPC subnet definitions." - type = list(object({ - subnet_name = string - subnet_ip = string - subnet_region = string - subnet_private_access = string - })) - default = [ - { - subnet_name = "networking" - subnet_ip = "10.0.0.0/24" - subnet_region = "europe-west1" - subnet_private_access = "true" - }, - { - subnet_name = "gce" - subnet_ip = "10.0.16.0/24" - subnet_region = "europe-west1" - subnet_private_access = "true" - }, - { - subnet_name = "gke" - subnet_ip = "10.0.32.0/24" - subnet_region = "europe-west1" - subnet_private_access = "true" - }, - ] -} - -variable "subnet_secondary_ranges" { - description = "Shared VPC subnets secondary range definitions." - type = map(list(object({ - range_name = string - ip_cidr_range = string - }))) - default = { - networking = [], - gce = [], - gke = [ - { - range_name = "services" - ip_cidr_range = "172.16.0.0/24" - }, - { - range_name = "pods" - ip_cidr_range = "10.128.0.0/18" - } - ] - } -} - -variable "project_services" { - description = "Service APIs enabled by default in new projects." - type = list(string) - default = [ - "resourceviews.googleapis.com", - "stackdriver.googleapis.com", - ] -} diff --git a/modules/README.md b/modules/README.md new file mode 100644 index 000000000..b99161368 --- /dev/null +++ b/modules/README.md @@ -0,0 +1,47 @@ +# 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. + +## Foundational modules + +- [folders](./modules/folders) +- [log sinks](./modules/logging-sinks) +- [project](./modules/project) +- [service accounts](./modules/iam-service-accounts) +- [ ] TODO: organization policies module + +## Networking modules + +- [address reservation](./modules/net-address) +- [Cloud DNS](./modules/dns) +- [Cloud NAT](./modules/net-cloudnat) +- [VPC](./modules/net-vpc) +- [VPC firewall](./modules/net-vpc-firewall) +- [VPC peering](./modules/net-vpc-peering) +- [VPN static](./modules/net-vpn-static) +- [VPN dynamic](./modules/net-vpn-dynamic) +- [VPN HA](./modules/net-vpn-ha)) +- [ ] TODO: xLB modules + +## Compute/Container + +- [COS container](./modules/compute-vm-cos-coredns) +- [GKE cluster](./modules/gke-cluster) +- [GKE nodepool](./modules/gke-nodepool) +- [VM/VM group](./modules/compute-vm) + +## Data + +- [BigQuery dataset](./modules/bigquery) +- [GCS](./modules/gcs) + +## Other + +- [Cloud KMS](./modules/kms) +- [on-premises in Docker](./modules/on-prem-in-a-box) diff --git a/modules/__experimental/cloud-function-scheduled/README.md b/modules/__experimental/cloud-function-scheduled/README.md new file mode 100644 index 000000000..df1edce00 --- /dev/null +++ b/modules/__experimental/cloud-function-scheduled/README.md @@ -0,0 +1,42 @@ +# Scheduled Google Cloud Function Module + +This module manages a background Cloud Function scheduled via a recurring Cloud Scheduler job. It also manages the required dependencies: a service account for the cloud function with optional IAM bindings, the PubSub topic used for the function trigger, and optionally the GCS bucket used for the code bundle. + +## Example + +```hcl +module "function" { + source = "./modules/cloud-function-scheduled" + project_id = "myproject" + name = "myfunction" + bundle_config = { + source_dir = "../cf" + output_path = "../bundle.zip" + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| bundle_config | Cloud function code bundle configuration, output path is a zip file. | object({...}) | ✓ | | +| name | Name used for resources (schedule, topic, etc.). | string | ✓ | | +| project_id | Project id used for all resources. | string | ✓ | | +| *bucket_name* | Name of the bucket that will be used for the function code, leave null to create one. | string | | null | +| *function_config* | Cloud function configuration. | object({...}) | | ... | +| *prefixes* | Optional prefixes for resource ids, null prefixes will be ignored. | object({...}) | | null | +| *region* | Region used for all resources. | string | | us-central1 | +| *schedule_config* | Cloud function scheduler job configuration, leave data null to pass the name variable, set schedule to null to disable schedule. | object({...}) | | ... | +| *service_account_iam_roles* | IAM roles assigned to the service account at the project level. | list(string) | | [] | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| bucket_name | Bucket name. | | +| function_name | Cloud function name. | | +| service_account_email | Service account email. | | +| topic_id | PubSub topic id. | | + diff --git a/modules/__experimental/cloud-function-scheduled/main.tf b/modules/__experimental/cloud-function-scheduled/main.tf new file mode 100644 index 000000000..0d3f03ff5 --- /dev/null +++ b/modules/__experimental/cloud-function-scheduled/main.tf @@ -0,0 +1,133 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + bucket = ( + var.bucket_name != null + ? var.bucket_name + : google_storage_bucket.bucket[0].name + ) + job_data = ( + var.schedule_config.pubsub_data == null || var.schedule_config.pubsub_data == "" + ? var.name + : var.schedule_config.pubsub_data + ) + prefixes = ( + var.prefixes == null + ? {} + : { + for k, v in var.prefixes : + k => v != null && v != "" ? "${v}-${var.name}" : var.name + } + ) + service_account = "serviceAccount:${google_service_account.service_account.email}" +} + +############################################################################### +# Scheduler / PubSub # +############################################################################### + +resource "google_pubsub_topic" "topic" { + project = var.project_id + name = lookup(local.prefixes, "topic", var.name) +} + +resource "google_cloud_scheduler_job" "job" { + count = var.schedule_config.schedule == null ? 0 : 1 + project = var.project_id + region = var.region + name = lookup(local.prefixes, "job", var.name) + schedule = var.schedule_config.schedule + time_zone = var.schedule_config.time_zone + + pubsub_target { + attributes = {} + topic_name = google_pubsub_topic.topic.id + data = base64encode(local.job_data) + } +} + +############################################################################### +# Cloud Function service account and IAM # +############################################################################### + +resource "google_service_account" "service_account" { + project = var.project_id + account_id = lookup(local.prefixes, "service_account", var.name) + display_name = "Terraform-managed" +} + +resource "google_project_iam_member" "service_account" { + for_each = toset(var.service_account_iam_roles) + project = var.project_id + role = each.value + member = local.service_account +} + +############################################################################### +# Cloud Function and GCS code bundle # +############################################################################### + +resource "google_cloudfunctions_function" "function" { + project = var.project_id + region = var.region + name = lookup(local.prefixes, "function", var.name) + description = "Terraform managed." + runtime = var.function_config.runtime + available_memory_mb = var.function_config.memory + max_instances = var.function_config.instances + timeout = var.function_config.timeout + entry_point = var.function_config.entry_point + service_account_email = google_service_account.service_account.email + + # source_repository { + # url = var.source_repository_url + # } + + event_trigger { + event_type = "providers/cloud.pubsub/eventTypes/topic.publish" + resource = google_pubsub_topic.topic.id + } + + source_archive_bucket = local.bucket + source_archive_object = google_storage_bucket_object.bundle.name +} + +resource "google_storage_bucket" "bucket" { + count = var.bucket_name == null ? 1 : 0 + project = var.project_id + name = lookup(local.prefixes, "bucket", var.name) + lifecycle_rule { + action { + type = "Delete" + } + condition { + age = "30" + } + } +} + +resource "google_storage_bucket_object" "bundle" { + name = "bundle-${data.archive_file.bundle.output_md5}.zip" + bucket = local.bucket + source = data.archive_file.bundle.output_path +} + +data "archive_file" "bundle" { + type = "zip" + source_dir = var.bundle_config.source_dir + output_path = var.bundle_config.output_path +} diff --git a/modules/__experimental/cloud-function-scheduled/outputs.tf b/modules/__experimental/cloud-function-scheduled/outputs.tf new file mode 100644 index 000000000..2d47bd09c --- /dev/null +++ b/modules/__experimental/cloud-function-scheduled/outputs.tf @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "bucket_name" { + description = "Bucket name." + value = local.bucket +} + +output "function_name" { + description = "Cloud function name." + value = google_cloudfunctions_function.function.name +} + +output "service_account_email" { + description = "Service account email." + value = google_service_account.service_account.email +} + +output "topic_id" { + description = "PubSub topic id." + value = google_pubsub_topic.topic.id +} diff --git a/modules/__experimental/cloud-function-scheduled/variables.tf b/modules/__experimental/cloud-function-scheduled/variables.tf new file mode 100644 index 000000000..78bb4b8ca --- /dev/null +++ b/modules/__experimental/cloud-function-scheduled/variables.tf @@ -0,0 +1,100 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "bucket_name" { + description = "Name of the bucket that will be used for the function code, leave null to create one." + type = string + default = null +} + +variable "bundle_config" { + description = "Cloud function code bundle configuration, output path is a zip file." + type = object({ + source_dir = string + output_path = string + }) +} + +variable "function_config" { + description = "Cloud function configuration." + type = object({ + entry_point = string + instances = number + memory = number + runtime = string + timeout = number + }) + default = { + entry_point = "main" + instances = 1 + memory = 256 + runtime = "python37" + timeout = 180 + } +} + +variable "name" { + description = "Name used for resources (schedule, topic, etc.)." + type = string +} + +variable "prefixes" { + description = "Optional prefixes for resource ids, null prefixes will be ignored." + type = object({ + bucket = string + function = string + job = string + service_account = string + topic = string + }) + default = null +} + +variable "project_id" { + description = "Project id used for all resources." + type = string +} + +variable "region" { + description = "Region used for all resources." + type = string + default = "us-central1" +} + +variable "schedule_config" { + description = "Cloud function scheduler job configuration, leave data null to pass the name variable, set schedule to null to disable schedule." + type = object({ + pubsub_data = string + schedule = string + time_zone = string + }) + default = { + schedule = "*/10 * * * *" + pubsub_data = null + time_zone = "UTC" + } +} + +variable "service_account_iam_roles" { + description = "IAM roles assigned to the service account at the project level." + type = list(string) + default = [] +} + +# variable "source_repository_url" { +# type = string +# default = "" +# } diff --git a/modules/__experimental/cloud-function-scheduled/versions.tf b/modules/__experimental/cloud-function-scheduled/versions.tf new file mode 100644 index 000000000..bc4c2a9d7 --- /dev/null +++ b/modules/__experimental/cloud-function-scheduled/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/bigquery/README.md b/modules/bigquery/README.md new file mode 100644 index 000000000..b894f1ae0 --- /dev/null +++ b/modules/bigquery/README.md @@ -0,0 +1,69 @@ +# Google Cloud Bigquery Module + +Simple Bigquery module offering support for multiple dataset creation and access configuration. + +The module interface is designed to allow setting default values for dataset access configurations and options (eg table or partition expiration), and optionally override them for individual datasets. Common labels applied to all datasets can also be specified with a single variable, and overridden individually. + +Access configuration supports specifying different [identity types](https://www.terraform.io/docs/providers/google/r/bigquery_dataset.html#access) via the `identity_type` attribute in access variables. The supported identity types are: `domain`, `group_by_email`, `special_group` (eg `projectOwners`), `user_by_email`. + +## Example + +```hcl +module "bigquery-datasets" { + source = "./modules/bigquery" + project_id = "my-project + datasets = { + dataset_1 = { + name = "Dataset 1." + description = "Terraform managed." + location = "EU" + labels = null + }, + dataset_2 = { + name = "Dataset 2." + description = "Terraform managed." + location = "EU" + labels = null + }, + } + default_access = [ + { + role = "OWNER" + identity_type = "special_group" + identity = "projectOwners" + } + ] + default_labels = { + eggs = "spam", + bar = "baz + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| datasets | Map of datasets to create keyed by id. Labels and options can be null. | map(object({...})) | ✓ | | +| project_id | Id of the project where datasets will be created. | string | ✓ | | +| *dataset_access* | Optional map of dataset access rules by dataset id. | map(list(object({...}))) | | {} | +| *dataset_options* | Optional map of dataset option by dataset id. | map(object({...})) | | {} | +| *default_access* | Access rules applied to all dataset if no specific ones are defined. | list(object({...})) | | [] | +| *default_labels* | Labels set on all datasets. | map(string) | | {} | +| *default_options* | Options used for all dataset if no specific ones are defined. | object({...}) | | ... | +| *kms_key* | Self link of the KMS key that will be used to protect destination table. | string | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| datasets | Dataset resources. | | +| ids | Dataset ids. | | +| names | Dataset names. | | +| self_links | Dataset self links. | | + + +## TODO + +- [ ] add support for tables diff --git a/modules/bigquery/main.tf b/modules/bigquery/main.tf new file mode 100644 index 000000000..9f42c231d --- /dev/null +++ b/modules/bigquery/main.tf @@ -0,0 +1,67 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + datasets = { + for id, data in var.datasets : + id => merge(data, { + options = lookup(var.dataset_options, id, var.default_options) + access = lookup(var.dataset_access, id, var.default_access) + labels = data.labels == null ? {} : data.labels + }) + } +} + +resource "google_bigquery_dataset" "datasets" { + for_each = local.datasets + project = var.project_id + dataset_id = each.key + friendly_name = each.value.name + description = each.value.description + labels = merge(var.default_labels, each.value.labels) + location = each.value.location + + delete_contents_on_destroy = each.value.options.delete_contents_on_destroy + default_table_expiration_ms = each.value.options.default_table_expiration_ms + default_partition_expiration_ms = each.value.options.default_partition_expiration_ms + + dynamic access { + for_each = each.value.access + iterator = config + content { + role = config.value.role + domain = each.value.identity_type == "domain" ? each.value.identity : null + group_by_email = each.value.identity_type == "group_by_email" ? each.value.identity : null + special_group = each.value.identity_type == "special_group" ? each.value.identity : null + user_by_email = each.value.identity_type == "user_by_email" ? each.value.identity : null + dynamic view { + for_each = each.value.identity_type == "view" ? [""] : [] + content { + project_id = view.value.project_id + dataset_id = view.value.dataset_id + table_id = view.value.table_id + } + } + } + } + + dynamic default_encryption_configuration { + for_each = var.kms_key == null ? [] : [""] + content { + kms_key_name = var.kms_key + } + } +} diff --git a/modules/bigquery/outputs.tf b/modules/bigquery/outputs.tf new file mode 100644 index 000000000..bffb0b39a --- /dev/null +++ b/modules/bigquery/outputs.tf @@ -0,0 +1,47 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "datasets" { + description = "Dataset resources." + value = [ + for _, resource in google_bigquery_dataset.datasets : + resource + ] +} + +output "ids" { + description = "Dataset ids." + value = [ + for _, resource in google_bigquery_dataset.datasets : + resource.id + ] +} + +output "names" { + description = "Dataset names." + value = [ + for _, resource in google_bigquery_dataset.datasets : + resource.friendly_name + ] +} + +output "self_links" { + description = "Dataset self links." + value = [ + for _, resource in google_bigquery_dataset.datasets : + resource.self_link + ] +} diff --git a/modules/bigquery/variables.tf b/modules/bigquery/variables.tf new file mode 100644 index 000000000..ad9ae4ae2 --- /dev/null +++ b/modules/bigquery/variables.tf @@ -0,0 +1,86 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "datasets" { + description = "Map of datasets to create keyed by id. Labels and options can be null." + type = map(object({ + description = string + location = string + name = string + labels = map(string) + })) +} + +variable "dataset_access" { + description = "Optional map of dataset access rules by dataset id." + type = map(list(object({ + role = string + identity_type = string + identity = any + }))) + default = {} +} + +variable "dataset_options" { + description = "Optional map of dataset option by dataset id." + type = map(object({ + default_table_expiration_ms = number + default_partition_expiration_ms = number + delete_contents_on_destroy = bool + })) + default = {} +} + +variable "default_access" { + description = "Access rules applied to all dataset if no specific ones are defined." + type = list(object({ + role = string + identity_type = string + identity = any + })) + default = [] +} + +variable "default_labels" { + description = "Labels set on all datasets." + type = map(string) + default = {} +} + +variable "default_options" { + description = "Options used for all dataset if no specific ones are defined." + type = object({ + default_table_expiration_ms = number + default_partition_expiration_ms = number + delete_contents_on_destroy = bool + }) + default = { + default_table_expiration_ms = null + default_partition_expiration_ms = null + delete_contents_on_destroy = false + } +} + +variable "kms_key" { + description = "Self link of the KMS key that will be used to protect destination table." + type = string + default = null +} + +variable "project_id" { + description = "Id of the project where datasets will be created." + type = string +} diff --git a/modules/bigquery/versions.tf b/modules/bigquery/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/bigquery/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/compute-vm/README.md b/modules/compute-vm/README.md new file mode 100644 index 000000000..e07680922 --- /dev/null +++ b/modules/compute-vm/README.md @@ -0,0 +1,113 @@ +# Google Compute Engine VM module + +This module allows creating one or multiple instances or an instance template for a specific configuration. A service account is optionally created and assigned if not specified. + +## TODO + +- [ ] add examples for instance groups + +## Examples + +### Instance leveraging defaults + +The simplest example leverages defaults for the boot disk image and size, and uses a service account created by the module. Multiple instances can be managed via the `instance_count` variable. + +```hcl +module "simple-vm-example" { + source = "../modules/compute-vm" + project_id = "my-project" + region = "europe-west1" + zone = "europe-west1-b" + name = "test" + network_interfaces = [{ + network = local.network_self_link, + subnetwork = local.subnet_self_link, + nat = false, + addresses = null + }] + service_account_create = true + instance_count = 1 +} +``` + +### Instance template + +This example shows how to use the module to manage an instance template that defines an additional attached disk for each instance, and overrides defaults for the boot disk image and service account. + +```hcl +module "debian-test" { + source = "../modules/compute-vm" + project_id = "my-project" + region = "europe-west1" + zone = "europe-west1-b" + name = "test" + network_interfaces = [{ + network = local.network_self_link, + subnetwork = local.subnet_self_link, + nat = false, + addresses = null + }] + instance_count = 1 + boot_disk = { + image = "projects/cos-cloud/global/images/family/cos-stable" + type = "pd-ssd" + size = 10 + } + attached_disks = [ + { name = "disk-1", size = 10, image = null, options = null } + ] + service_account = "vm-default@my-project.iam.gserviceaccount.com" + use_instance_template = true +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| name | Instances base name. | string | ✓ | | +| network_interfaces | Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed. | list(object({...})) | ✓ | | +| project_id | Project id. | string | ✓ | | +| region | Compute region. | string | ✓ | | +| zone | Compute zone. | string | ✓ | | +| *attached_disk_defaults* | Defaults for attached disks options. | object({...}) | | ... | +| *attached_disks* | Additional disks, if options is null defaults will be used in its place. | list(object({...})) | | [] | +| *boot_disk* | Boot disk properties. | object({...}) | | ... | +| *group* | Instance group (for instance use). | object({...}) | | null | +| *group_manager* | Instance group manager (for template use). | object({...}) | | null | +| *hostname* | Instance FQDN name. | string | | null | +| *instance_count* | Number of instances to create (only for non-template usage). | number | | 1 | +| *instance_type* | Instance type. | string | | f1-micro | +| *labels* | Instance labels. | map(string) | | {} | +| *metadata* | Instance metadata. | map(string) | | {} | +| *min_cpu_platform* | Minimum CPU platform. | string | | null | +| *options* | Instance options. | object({...}) | | ... | +| *scratch_disks* | Scratch disks configuration. | object({...}) | | ... | +| *service_account* | Service account email. Unused if service account is auto-created. | string | | null | +| *service_account_create* | Auto-create service account. | bool | | false | +| *service_account_scopes* | Scopes applied to service account. | list(string) | | [] | +| *tags* | Instance tags. | list(string) | | [] | +| *use_instance_template* | Create instance template instead of instances. | bool | | false | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| external_ips | Instance main interface external IP addresses. | | +| group | Instance group resource. | | +| group_manager | Instance group resource. | | +| instances | Instance resources. | | +| internal_ips | Instance main interface internal IP addresses. | | +| names | Instance names. | | +| self_links | Instance self links. | | +| service_account | Service account resource. | | +| service_account_email | Service account email. | | +| service_account_iam_email | Service account email. | | +| template | Template resource. | | +| template_name | Template name. | | + + +## TODO + +- [ ] add support for instance groups diff --git a/modules/compute-vm/instance_group.tf b/modules/compute-vm/instance_group.tf new file mode 100644 index 000000000..17dcd44db --- /dev/null +++ b/modules/compute-vm/instance_group.tf @@ -0,0 +1,215 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_instance_group" "unmanaged" { + count = ( + var.group != null && ! var.use_instance_template ? 1 : 0 + ) + project = var.project_id + network = ( + length(var.network_interfaces) > 0 + ? var.network_interfaces.0.network + : "" + ) + zone = var.zone + name = var.name + description = "Terraform-managed." + instances = [ + for name, instance in google_compute_instance.default : instance.self_link + ] + dynamic named_port { + for_each = var.group.named_ports != null ? var.group.named_ports : {} + iterator = config + content { + name = config.key + port = config.value + } + } +} + +resource "google_compute_instance_group_manager" "managed" { + count = ( + var.group_manager != null && var.use_instance_template + ? var.group_manager.regional ? 0 : 1 + : 0 + ) + project = var.project_id + zone = var.zone + name = var.name + base_instance_name = var.name + description = "Terraform-managed." + target_size = var.group_manager.target_size + target_pools = ( + var.group_manager.options == null + ? null + : var.group_manager.options.target_pools + ) + wait_for_instances = ( + var.group_manager.options == null + ? null + : var.group_manager.options.wait_for_instances + ) + dynamic auto_healing_policies { + for_each = ( + var.group_manager.auto_healing_policies == null + ? [] + : [var.group_manager.auto_healing_policies] + ) + iterator = config + content { + health_check = config.value.health_check + initial_delay_sec = config.value.initial_delay_sec + } + } + dynamic update_policy { + for_each = ( + var.group_manager.update_policy == null + ? [] + : [var.group_manager.update_policy] + ) + iterator = config + content { + type = config.value.type + minimal_action = config.value.minimal_action + min_ready_sec = config.value.min_ready_sec + max_surge_fixed = ( + config.value.max_surge_type == "fixed" ? config.value.max_surge : null + ) + max_surge_percent = ( + config.value.max_surge_type == "percent" ? config.value.max_surge : null + ) + max_unavailable_fixed = ( + config.value.max_unavailable_type == "fixed" ? config.value.max_unavailable : null + ) + max_unavailable_percent = ( + config.value.max_unavailable_type == "percent" ? config.value.max_unavailable : null + ) + } + } + dynamic named_port { + for_each = var.group_manager.named_ports != null ? var.group_manager.named_ports : {} + iterator = config + content { + name = config.key + port = config.value + } + } + version { + name = "${var.name}-default" + instance_template = google_compute_instance_template.default.0.self_link + } + dynamic version { + for_each = ( + var.group_manager.versions == null ? [] : [var.group_manager.versions] + ) + iterator = config + content { + name = config.value.name + instance_template = config.value.instance_template + target_size { + fixed = config.value.target_type == "fixed" ? config.value.target_size : null + percent = config.value.target_type == "percent" ? config.value.target_size : null + } + } + } +} + +resource "google_compute_region_instance_group_manager" "managed" { + count = ( + var.group_manager != null && var.use_instance_template + ? var.group_manager.regional ? 1 : 0 + : 0 + ) + project = var.project_id + region = var.region + name = var.name + base_instance_name = var.name + description = "Terraform-managed." + target_size = var.group_manager.target_size + target_pools = ( + var.group_manager.options == null + ? null + : var.group_manager.options.target_pools + ) + wait_for_instances = ( + var.group_manager.options == null + ? null + : var.group_manager.options.wait_for_instances + ) + dynamic auto_healing_policies { + for_each = ( + var.group_manager.auto_healing_policies == null + ? [] + : [var.group_manager.auto_healing_policies] + ) + iterator = config + content { + health_check = config.value.health_check + initial_delay_sec = config.value.initial_delay_sec + } + } + dynamic update_policy { + for_each = ( + var.group_manager.update_policy == null + ? [] + : [var.group_manager.update_policy] + ) + iterator = config + content { + type = config.value.type + minimal_action = config.value.minimal_action + min_ready_sec = config.value.min_ready_sec + max_surge_fixed = ( + config.value.max_surge_type == "fixed" ? config.value.max_surge : null + ) + max_surge_percent = ( + config.value.max_surge_type == "percent" ? config.value.max_surge : null + ) + max_unavailable_fixed = ( + config.value.max_unavailable_type == "fixed" ? config.value.max_unavailable : null + ) + max_unavailable_percent = ( + config.value.max_unavailable_type == "percent" ? config.value.max_unavailable : null + ) + } + } + dynamic named_port { + for_each = var.group.named_ports + iterator = config + content { + name = config.key + port = config.value + } + } + version { + name = "${var.name}-default" + instance_template = google_compute_instance_template.default.0.self_link + } + dynamic version { + for_each = ( + var.group_manager.versions == null ? [] : [var.group_manager.versions] + ) + iterator = config + content { + name = config.value.name + instance_template = config.value.instance_template + target_size { + fixed = config.value.target_type == "fixed" ? config.value.target_size : null + percent = config.value.target_type == "percent" ? config.value.target_size : null + } + } + } +} diff --git a/modules/compute-vm/main.tf b/modules/compute-vm/main.tf new file mode 100644 index 000000000..75f642dc8 --- /dev/null +++ b/modules/compute-vm/main.tf @@ -0,0 +1,226 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + attached_disks = { + for disk in var.attached_disks : + disk.name => merge(disk, { + options = disk.options == null ? var.attached_disk_defaults : disk.options + }) + } + attached_disks_pairs = { + for pair in setproduct(keys(local.names), keys(local.attached_disks)) : + "${pair[0]}-${pair[1]}" => { name = pair[0], disk_name = pair[1] } + } + names = ( + var.use_instance_template + ? { "${var.name}" = 0 } + : { for i in range(0, var.instance_count) : "${var.name}-${i + 1}" => i } + ) + 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 + ) + service_account_scopes = ( + length(var.service_account_scopes) > 0 + ? var.service_account_scopes + : ( + var.service_account_create + ? ["https://www.googleapis.com/auth/cloud-platform"] + : [ + "https://www.googleapis.com/auth/devstorage.read_only", + "https://www.googleapis.com/auth/logging.write", + "https://www.googleapis.com/auth/monitoring.write" + ] + ) + ) +} + +resource "google_compute_disk" "disks" { + for_each = var.use_instance_template ? {} : local.attached_disks_pairs + project = var.project_id + zone = var.zone + name = each.key + type = local.attached_disks[each.value.disk_name].options.type + size = local.attached_disks[each.value.disk_name].size + labels = merge(var.labels, { + disk_name = local.attached_disks[each.value.disk_name].name + disk_type = local.attached_disks[each.value.disk_name].options.type + image = local.attached_disks[each.value.disk_name].image + }) +} + +resource "google_compute_instance" "default" { + for_each = var.use_instance_template ? {} : local.names + project = var.project_id + zone = var.zone + name = each.key + hostname = var.hostname + description = "Managed by the compute-vm Terraform module." + tags = var.tags + machine_type = var.instance_type + min_cpu_platform = var.min_cpu_platform + can_ip_forward = var.options.can_ip_forward + allow_stopping_for_update = var.options.allow_stopping_for_update + deletion_protection = var.options.deletion_protection + metadata = var.metadata + labels = var.labels + + dynamic attached_disk { + for_each = { + for resource_name, pair in local.attached_disks_pairs : + resource_name => local.attached_disks[pair.disk_name] if pair.name == each.key + } + iterator = config + content { + device_name = config.value.name + mode = config.value.options.mode + source = google_compute_disk.disks[config.key].name + } + } + + boot_disk { + initialize_params { + type = var.boot_disk.type + image = var.boot_disk.image + size = var.boot_disk.size + } + } + + dynamic network_interface { + for_each = var.network_interfaces + iterator = config + content { + network = config.value.network + subnetwork = config.value.subnetwork + network_ip = config.value.addresses == null ? null : ( + length(config.value.addresses.internal) == 0 + ? null + : config.value.addresses.internal[each.value] + ) + dynamic access_config { + for_each = config.value.nat ? [config.value.addresses] : [] + iterator = nat_addresses + content { + nat_ip = nat_addresses.value == null ? null : ( + length(nat_addresses.value) == 0 ? null : nat_addresses.value[each.value] + ) + } + } + } + } + + scheduling { + automatic_restart = ! var.options.preemptible + on_host_maintenance = var.options.preemptible ? "TERMINATE" : "MIGRATE" + preemptible = var.options.preemptible + } + + dynamic scratch_disk { + for_each = [ + for i in range(0, var.scratch_disks.count) : var.scratch_disks.interface + ] + iterator = config + content { + interface = config.value + } + } + + service_account { + email = local.service_account_email + scopes = local.service_account_scopes + } + + # guest_accelerator + # shielded_instance_config + +} + +resource "google_compute_instance_template" "default" { + count = var.use_instance_template ? 1 : 0 + project = var.project_id + region = var.region + name_prefix = "${var.name}-" + description = "Managed by the compute-vm Terraform module." + tags = var.tags + machine_type = var.instance_type + min_cpu_platform = var.min_cpu_platform + can_ip_forward = var.options.can_ip_forward + metadata = var.metadata + labels = var.labels + + disk { + source_image = var.boot_disk.image + disk_type = var.boot_disk.type + disk_size_gb = var.boot_disk.size + boot = true + } + + dynamic disk { + for_each = local.attached_disks + iterator = config + content { + auto_delete = config.value.options.auto_delete + device_name = config.value.name + disk_type = config.value.options.type + disk_size_gb = config.value.size + mode = config.value.options.mode + source_image = config.value.image + source = config.value.options.source + type = "PERSISTENT" + } + } + + dynamic network_interface { + for_each = var.network_interfaces + iterator = config + content { + network = config.value.network + subnetwork = config.value.subnetwork + dynamic access_config { + for_each = config.value.nat ? [""] : [] + content {} + } + } + } + + scheduling { + automatic_restart = ! var.options.preemptible + on_host_maintenance = var.options.preemptible ? "TERMINATE" : "MIGRATE" + preemptible = var.options.preemptible + } + + service_account { + email = local.service_account_email + scopes = local.service_account_scopes + } + + lifecycle { + create_before_destroy = true + } +} + +resource "google_service_account" "service_account" { + count = var.service_account_create ? 1 : 0 + project = var.project_id + account_id = "tf-vm-${var.name}" + display_name = "Terraform VM ${var.name}." +} diff --git a/modules/compute-vm/outputs.tf b/modules/compute-vm/outputs.tf new file mode 100644 index 000000000..e927c1596 --- /dev/null +++ b/modules/compute-vm/outputs.tf @@ -0,0 +1,110 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "external_ips" { + description = "Instance main interface external IP addresses." + value = ( + var.network_interfaces[0].nat + ? [ + for name, instance in google_compute_instance.default : + instance.network_interface.0.network_ip + ] + : [] + ) +} + +output "group" { + description = "Instance group resource." + value = ( + length(google_compute_instance_group.unmanaged) > 0 + ? google_compute_instance_group.unmanaged.0 + : null + ) +} + +output "group_manager" { + description = "Instance group resource." + value = ( + length(google_compute_instance_group_manager.managed) > 0 + ? google_compute_instance_group_manager.managed.0 + : ( + length(google_compute_region_instance_group_manager.managed) > 0 + ? google_compute_region_instance_group_manager.managed.0 + : null + ) + ) +} + +output "instances" { + description = "Instance resources." + value = [for name, instance in google_compute_instance.default : instance] +} + +output "internal_ips" { + description = "Instance main interface internal IP addresses." + value = [ + for name, instance in google_compute_instance.default : + instance.network_interface.0.network_ip + ] +} + +output "names" { + description = "Instance names." + value = [for name, instance in google_compute_instance.default : instance.name] +} + +output "self_links" { + description = "Instance self links." + value = [for name, instance in google_compute_instance.default : instance.self_link] +} + +output "service_account" { + description = "Service account resource." + value = ( + var.service_account_create ? google_service_account.service_account[0] : null + ) +} + +output "service_account_email" { + description = "Service account email." + value = local.service_account_email +} + +output "service_account_iam_email" { + description = "Service account email." + value = join("", [ + "serviceAccount:", + local.service_account_email == null ? "" : local.service_account_email + ]) +} + +output "template" { + description = "Template resource." + value = ( + length(google_compute_instance_template.default) > 0 + ? google_compute_instance_template.default[0] + : null + ) +} + +output "template_name" { + description = "Template name." + value = ( + length(google_compute_instance_template.default) > 0 + ? google_compute_instance_template.default[0].name + : null + ) +} diff --git a/modules/compute-vm/variables.tf b/modules/compute-vm/variables.tf new file mode 100644 index 000000000..018e16d10 --- /dev/null +++ b/modules/compute-vm/variables.tf @@ -0,0 +1,231 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "attached_disks" { + description = "Additional disks, if options is null defaults will be used in its place." + type = list(object({ + name = string + image = string + size = string + options = object({ + auto_delete = bool + mode = string + source = string + type = string + }) + })) + default = [] +} + +variable "attached_disk_defaults" { + description = "Defaults for attached disks options." + type = object({ + auto_delete = bool + mode = string + type = string + source = string + }) + default = { + auto_delete = true + source = null + mode = "READ_WRITE" + type = "pd-ssd" + } +} + +variable "boot_disk" { + description = "Boot disk properties." + type = object({ + image = string + size = number + type = string + }) + default = { + image = "projects/debian-cloud/global/images/family/debian-10" + type = "pd-ssd" + size = 10 + } +} + +variable "group" { + description = "Instance group (for instance use)." + type = object({ + named_ports = map(number) + }) + default = null +} + +variable "group_manager" { + description = "Instance group manager (for template use)." + type = object({ + auto_healing_policies = object({ + health_check = string + initial_delay_sec = number + }) + named_ports = map(number) + options = object({ + target_pools = list(string) + wait_for_instances = bool + }) + regional = bool + target_size = number + update_policy = object({ + type = string # OPPORTUNISTIC | PROACTIVE + minimal_action = string # REPLACE | RESTART + min_ready_sec = number + max_surge_type = string # fixed | percent + max_surge = number + max_unavailable_type = string + max_unavailable = number + }) + versions = list(object({ + name = string + instance_template = string + target_type = string # fixed | percent + target_size = number + })) + }) + default = null +} + +variable "hostname" { + description = "Instance FQDN name." + type = string + default = null +} + +variable "instance_count" { + description = "Number of instances to create (only for non-template usage)." + type = number + default = 1 +} + +variable "instance_type" { + description = "Instance type." + type = string + default = "f1-micro" +} + +variable "labels" { + description = "Instance labels." + type = map(string) + default = {} +} + +variable "metadata" { + description = "Instance metadata." + type = map(string) + default = {} +} + +variable "min_cpu_platform" { + description = "Minimum CPU platform." + type = string + default = null +} + +variable "name" { + description = "Instances base name." + type = string +} + +variable "network_interfaces" { + description = "Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed." + type = list(object({ + nat = bool + network = string + subnetwork = string + addresses = object({ + internal = list(string) + external = list(string) + }) + })) +} + +variable "options" { + description = "Instance options." + type = object({ + allow_stopping_for_update = bool + can_ip_forward = bool + deletion_protection = bool + preemptible = bool + }) + default = { + allow_stopping_for_update = true + can_ip_forward = false + deletion_protection = false + preemptible = false + } +} + +variable "project_id" { + description = "Project id." + type = string +} + +variable "region" { + description = "Compute region." + type = string +} + +variable "scratch_disks" { + description = "Scratch disks configuration." + type = object({ + count = number + interface = string + }) + default = { + count = 0 + interface = "NVME" + } +} + +variable "service_account" { + description = "Service account email. Unused if service account is auto-created." + type = string + default = null +} + +variable "service_account_create" { + description = "Auto-create service account." + type = bool + default = false +} + +# scopes and scope aliases list +# https://cloud.google.com/sdk/gcloud/reference/compute/instances/create#--scopes +variable "service_account_scopes" { + description = "Scopes applied to service account." + type = list(string) + default = [] +} + +variable "tags" { + description = "Instance tags." + type = list(string) + default = [] +} + +variable "use_instance_template" { + description = "Create instance template instead of instances." + type = bool + default = false +} + +variable "zone" { + description = "Compute zone." + type = string +} diff --git a/modules/cos-container/.gitignore b/modules/cos-container/.gitignore new file mode 100644 index 000000000..bbd801c73 --- /dev/null +++ b/modules/cos-container/.gitignore @@ -0,0 +1 @@ +**/test.tf diff --git a/modules/cos-container/README.md b/modules/cos-container/README.md new file mode 100644 index 000000000..793e52a5c --- /dev/null +++ b/modules/cos-container/README.md @@ -0,0 +1,25 @@ +# Container Optimized OS modules + +This set of modules creates specialized [cloud-config](https://cloud.google.com/container-optimized-os/docs/how-to/run-container-instance#starting_a_docker_container_via_cloud-config) configurations for [Container Optimized OS](https://cloud.google.com/container-optimized-os/docs), that are used to quickly spin up containerized services for DNS, HTTP, or databases. + +It's meant to fullfill different use cases: + +- when designing, to quickly prototype specialized services (eg MySQL access or HTTP serving) +- when planning migrations, to emulate production services for core infrastructure or perfomance testing +- in production, to easily add glue components for services like DNS (eg to work around inbound/outbound forwarding limitations) +- as a basis to implement cloud-native production deployments that leverage cloud-init for configuration management + +## Available modules + +- [CoreDNS](./coredns) +- [MySQL](./mysql) +- [ ] Nginx +- [ ] Squid forward proxy + +## Using the modules + +All modules are designed to be as lightweight as possible, so that specialized modules like [compute-vm](../compute-vm) can be leveraged to manage instances or instance templates, and to allow simple forking to create custom derivatives. + +To use the modules with instances or instance templates, simply set use their `cloud_config` output for the `user-data` metadata. When updating the metadata after a variable change remember to manually restart the instances that use a module's output, or the changes won't effect the running system. + +For convenience when developing or prototyping infrastructure, an optional test instance is included in all modules. If it's not needed, the linked `*instance.tf` files can be removed from the modules without harm. diff --git a/modules/cos-container/coredns/Corefile b/modules/cos-container/coredns/Corefile new file mode 100644 index 000000000..e5a7674fa --- /dev/null +++ b/modules/cos-container/coredns/Corefile @@ -0,0 +1,6 @@ +. { + forward . /etc/resolv.conf + reload + log + errors +} \ No newline at end of file diff --git a/modules/cos-container/coredns/Corefile-hosts b/modules/cos-container/coredns/Corefile-hosts new file mode 100644 index 000000000..1baa581a6 --- /dev/null +++ b/modules/cos-container/coredns/Corefile-hosts @@ -0,0 +1,9 @@ +. { + hosts /etc/coredns/example.hosts example.org { + 127.0.0.1 localhost.example.org localhost + } + forward . /etc/resolv.conf + reload + log + errors +} diff --git a/modules/cos-container/coredns/README.md b/modules/cos-container/coredns/README.md new file mode 100644 index 000000000..87fda8f47 --- /dev/null +++ b/modules/cos-container/coredns/README.md @@ -0,0 +1,93 @@ +# Containerized CoreDNS on Container Optimized OS + +This module manages a `cloud-config` configuration that starts a containerized [CoreDNS](https://coredns.io/) service on Container Optimized OS, using the [official image](https://hub.docker.com/r/coredns/coredns/). + +The resulting `cloud-config` can be customized in a number of ways: + +- a custom CoreDNS configuration can be set using the `coredns_config` variable +- additional files (eg for hosts or zone files) can be passed in via the `files` variable +- a completely custom `cloud-config` can be passed in via the `cloud_config` variable, and additional template variables can be passed in via `config_variables` + +The default instance configuration inserts iptables rules to allow traffic on the DNS TCP and UDP ports, and the 8080 port for the optional HTTP health check that can be enabled via the CoreDNS [health plugin](https://coredns.io/plugins/health/). + +Logging and monitoring are enabled via the [Google Cloud Logging driver](https://docs.docker.com/config/containers/logging/gcplogs/) configured for the CoreDNS container, and the [Node Problem Detector](https://cloud.google.com/container-optimized-os/docs/how-to/monitoring) service started by default on boot. + +The module renders the generated cloud config in the `cloud_config` output, to be used in instances or instance templates via the `user-data` metadata. + +For convenience during development or for simple use cases, the module can optionally manage a single instance via the `test_instance` variable. If the instance is not needed the `instance*tf` files can be safely removed. Refer to the [top-level README](../README.md) for more details on the included instance. + +## Examples + +### Default CoreDNS configuration + +This example will create a `cloud-config` that uses the module's defaults, creating a simple DNS forwarder. + +```hcl +module "cos-coredns" { + source = "./modules/cos-container/coredns" +} + +# use it as metadata in a compute instance or template +resource "google_compute_instance" "default" { + metadata = { + user-data = module.cos-coredns.cloud_config + } +``` + +### Custom CoreDNS configuration + +This example will create a `cloud-config` using a custom CoreDNS configuration, that leverages the [CoreDNS hosts plugin]() to serve a single zone via an included `hosts` format file. + +```hcl +module "cos-coredns" { + source = "./modules/cos-container/coredns" + coredns_config = "./modules/cos-container/coredns/Corefile-hosts" + files = { + "/etc/coredns/example.hosts" = { + content = "127.0.0.2 foo.example.org foo" + owner = null + permissions = "0644" + } +} +``` + +### CoreDNS instance + +This example shows how to create the single instance optionally managed by the module, providing all required attributes in the `test_instance` variable. The instance is purposefully kept simple and should only be used in development, or when designing infrastructures. + +```hcl +module "cos-coredns" { + source = "./modules/cos-container/coredns" + test_instance = { + project_id = "my-project" + zone = "europe-west1-b" + name = "cos-coredns" + type = "f1-micro" + tags = ["ssh"] + metadata = {} + network = "default" + subnetwork = "https://www.googleapis.com/compute/v1/projects/my-project/regions/europe-west1/subnetworks/my-subnet" + disks = [] + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| *cloud_config* | Cloud config template path. If null default will be used. | string | | null | +| *config_variables* | Additional variables used to render the cloud-config template. | map(any) | | {} | +| *coredns_config* | CoreDNS configuration path, if null default will be used. | string | | null | +| *file_defaults* | Default owner and permissions for files. | object({...}) | | ... | +| *files* | Map of extra files to create on the instance, path as key. Owner and permissions will use defaults if null. | map(object({...})) | | {} | +| *test_instance* | Test/development instance attributes, leave null to skip creation. | object({...}) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| cloud_config | Rendered cloud-config file to be passed as user-data instance metadata. | | +| test_instance | Optional test instance name and address | | + diff --git a/modules/cos-container/coredns/cloud-config.yaml b/modules/cos-container/coredns/cloud-config.yaml new file mode 100644 index 000000000..b8796c527 --- /dev/null +++ b/modules/cos-container/coredns/cloud-config.yaml @@ -0,0 +1,83 @@ +#cloud-config + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.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. + +# https://hub.docker.com/r/coredns/coredns/ +# https://coredns.io/manual/toc/#installation + +# TODO: switch to the gcplogs logging driver, and set driver labels + +write_files: + - path: /var/lib/docker/daemon.json + permissions: 0644 + owner: root + content: | + { + "live-restore": true, + "storage-driver": "overlay2", + "log-opts": { + "max-size": "1024m" + } + } + + # disable systemd-resolved to free port 53 on the loopback interface + - path: /etc/systemd/resolved.conf + permissions: 0644 + owner: root + content: | + [Resolve] + LLMNR=no + DNSStubListener=no + + - path: /etc/coredns/Corefile + permissions: 0644 + owner: root + content: | + ${indent(6, corefile)} + + # coredns container service + - path: /etc/systemd/system/coredns.service + permissions: 0644 + owner: root + content: | + [Unit] + Description=Start CoreDNS container + After=gcr-online.target docker.socket + Wants=gcr-online.target docker.socket docker-events-collector.service + [Service] + ExecStart=/usr/bin/docker run --rm --name=coredns \ + --log-driver=gcplogs --network host \ + -v /etc/coredns:/etc/coredns \ + coredns/coredns -conf /etc/coredns/Corefile + ExecStop=/usr/bin/docker stop coredns + + %{ for path, data in files } + - path: ${path} + owner: ${lookup(data, "owner", "root")} + permissions: ${lookup(data, "permissions", "0644")} + content: | + ${indent(4, data.content)} + %{ endfor } + +bootcmd: + - systemctl start node-problem-detector + +runcmd: + - iptables -I INPUT 1 -p tcp -m tcp --dport 8080 -m state --state NEW,ESTABLISHED -j ACCEPT + - iptables -I INPUT 1 -p tcp -m tcp --dport 53 -m state --state NEW,ESTABLISHED -j ACCEPT + - iptables -I INPUT 1 -p udp -m udp --dport 53 -m state --state NEW,ESTABLISHED -j ACCEPT + - systemctl daemon-reload + - systemctl restart systemd-resolved.service + - systemctl start coredns \ No newline at end of file diff --git a/modules/cos-container/coredns/instance.tf b/modules/cos-container/coredns/instance.tf new file mode 120000 index 000000000..bdef596b6 --- /dev/null +++ b/modules/cos-container/coredns/instance.tf @@ -0,0 +1 @@ +../instance.tf \ No newline at end of file diff --git a/modules/cos-container/coredns/main.tf b/modules/cos-container/coredns/main.tf new file mode 100644 index 000000000..c79355771 --- /dev/null +++ b/modules/cos-container/coredns/main.tf @@ -0,0 +1,41 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + cloud_config = templatefile(local.template, merge(var.config_variables, { + corefile = local.corefile + files = local.files + })) + corefile = file( + var.coredns_config == null ? "${path.module}/Corefile" : var.coredns_config + ) + files = { + for path, attrs in var.files : path => { + content = attrs.content, + owner = attrs.owner == null ? var.file_defaults.owner : attrs.owner, + permissions = ( + attrs.permissions == null + ? var.file_defaults.permissions + : attrs.permissions + ) + } + } + template = ( + var.cloud_config == null + ? "${path.module}/cloud-config.yaml" + : var.cloud_config + ) +} diff --git a/modules/cos-container/coredns/outputs-instance.tf b/modules/cos-container/coredns/outputs-instance.tf new file mode 120000 index 000000000..ea9e24045 --- /dev/null +++ b/modules/cos-container/coredns/outputs-instance.tf @@ -0,0 +1 @@ +../outputs-instance.tf \ No newline at end of file diff --git a/modules/cos-container/coredns/outputs.tf b/modules/cos-container/coredns/outputs.tf new file mode 100644 index 000000000..205a55716 --- /dev/null +++ b/modules/cos-container/coredns/outputs.tf @@ -0,0 +1,20 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "cloud_config" { + description = "Rendered cloud-config file to be passed as user-data instance metadata." + value = local.cloud_config +} diff --git a/modules/cos-container/coredns/variables-instance.tf b/modules/cos-container/coredns/variables-instance.tf new file mode 120000 index 000000000..94af61e4d --- /dev/null +++ b/modules/cos-container/coredns/variables-instance.tf @@ -0,0 +1 @@ +../variables-instance.tf \ No newline at end of file diff --git a/modules/cos-container/coredns/variables.tf b/modules/cos-container/coredns/variables.tf new file mode 100644 index 000000000..f50d85826 --- /dev/null +++ b/modules/cos-container/coredns/variables.tf @@ -0,0 +1,55 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "cloud_config" { + description = "Cloud config template path. If null default will be used." + type = string + default = null +} + +variable "config_variables" { + description = "Additional variables used to render the cloud-config template." + type = map(any) + default = {} +} + +variable "coredns_config" { + description = "CoreDNS configuration path, if null default will be used." + type = string + default = null +} + +variable "file_defaults" { + description = "Default owner and permissions for files." + type = object({ + owner = string + permissions = string + }) + default = { + owner = "root" + permissions = "0644" + } +} + +variable "files" { + description = "Map of extra files to create on the instance, path as key. Owner and permissions will use defaults if null." + type = map(object({ + content = string + owner = string + permissions = string + })) + default = {} +} diff --git a/modules/cos-container/instance.tf b/modules/cos-container/instance.tf new file mode 100644 index 000000000..294167c47 --- /dev/null +++ b/modules/cos-container/instance.tf @@ -0,0 +1,87 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + disks = var.test_instance == null ? {} : var.test_instance.disks + sa_roles = ["roles/logging.logWriter", "roles/monitoring.metricWriter"] +} + +resource "google_service_account" "default" { + count = var.test_instance == null ? 0 : 1 + project = var.test_instance.project_id + account_id = "cos-test-${var.test_instance.name}" + display_name = "Managed by the cos Terraform module." +} + +resource "google_project_iam_member" "default" { + for_each = var.test_instance == null ? toset([]) : toset(local.sa_roles) + project = var.test_instance.project_id + role = each.value + member = "serviceAccount:${google_service_account.default[0].email}" +} + +resource "google_compute_disk" "disks" { + for_each = local.disks + project = var.test_instance.project_id + zone = var.test_instance.zone + name = each.key + type = "pd-ssd" + size = each.value.size +} + +resource "google_compute_instance" "default" { + count = var.test_instance == null ? 0 : 1 + project = var.test_instance.project_id + zone = var.test_instance.zone + name = var.test_instance.name + description = "Managed by the cos Terraform module." + tags = var.test_instance.tags + machine_type = ( + var.test_instance.type == null ? "f1-micro" : var.test_instance.type + ) + metadata = merge(var.test_instance.metadata, { + user-data = local.cloud_config + }) + + dynamic attached_disk { + for_each = local.disks + iterator = disk + content { + device_name = disk.key + mode = disk.value.read_only ? "READ_ONLY" : "READ_WRITE" + source = google_compute_disk.disks[disk.key].name + } + } + + boot_disk { + initialize_params { + type = "pd-ssd" + image = "projects/cos-cloud/global/images/family/cos-stable" + size = 10 + } + } + + network_interface { + network = var.test_instance.network + subnetwork = var.test_instance.subnetwork + } + + service_account { + email = google_service_account.default[0].email + scopes = ["https://www.googleapis.com/auth/cloud-platform"] + } + +} diff --git a/modules/cos-container/mysql/.gitignore b/modules/cos-container/mysql/.gitignore new file mode 100644 index 000000000..95ea22d21 --- /dev/null +++ b/modules/cos-container/mysql/.gitignore @@ -0,0 +1,2 @@ +kms.tf +kms.tf.sample diff --git a/modules/cos-container/mysql/README.md b/modules/cos-container/mysql/README.md new file mode 100644 index 000000000..cfcfca3a9 --- /dev/null +++ b/modules/cos-container/mysql/README.md @@ -0,0 +1,100 @@ +# Containerized MySQL on Container Optimized OS + +This module manages a `cloud-config` configuration that starts a containerized [MySQL](https://www.mysql.com/) service on Container Optimized OS, using the [official image](https://hub.docker.com/_/mysql). + +The resulting `cloud-config` can be customized in a number of ways: + +- a custom MySQL configuration can be set using the `mysql_config` variable +- the container image can be changed via the `image` variable +- a data disk can be specified via the `mysql_data_disk` variable, the configuration will optionally format and mount it for container use +- a KMS encrypted root password can be passed to the container image, and decrypted at runtime on the instance using the attributes in the `kms_config` variable +- a completely custom `cloud-config` can be passed in via the `cloud_config` variable, and additional template variables can be passed in via `config_variables` + +The default instance configuration inserts a sngle iptables rule to allow traffic on the default MySQL port. + +Logging and monitoring are enabled via the [Google Cloud Logging driver](https://docs.docker.com/config/containers/logging/gcplogs/) configured for the CoreDNS container, and the [Node Problem Detector](https://cloud.google.com/container-optimized-os/docs/how-to/monitoring) service started by default on boot. + +The module renders the generated cloud config in the `cloud_config` output, to be used in instances or instance templates via the `user-data` metadata. + +For convenience during development or for simple use cases, the module can optionally manage a single instance via the `test_instance` variable. Please note that an `f1-micro` instance is too small to run MySQL. If the instance is not needed the `instance*tf` files can be safely removed. Refer to the [top-level README](../README.md) for more details on the included instance. + +## Examples + +### Default MySQL configuration + +This example will create a `cloud-config` that uses the container's default configuration, and a plaintext password for the MySQL root user. + +```hcl +module "cos-mysql" { + source = "./modules/cos-container/mysql" + mysql_password = "foo" +} + +# use it as metadata in a compute instance or template +resource "google_compute_instance" "default" { + metadata = { + user-data = module.cos-mysql.cloud_config + } +``` + +### Custom MySQL configuration and KMS encrypted password + +This example will create a `cloud-config` that uses a custom MySQL configuration, and passes in an encrypted password and the KMS attributes required to decrypt it. Please note that the instance service account needs the `roles/cloudkms.cryptoKeyDecrypter` on the specified KMS key. + +```hcl +module "cos-mysql" { + source = "./modules/cos-container/mysql" + mysql_config = "./my.cnf" + mysql_password = "CiQAsd7WY==" + kms_config = { + project_id = "my-project" + keyring = "test-cos" + location = "europe-west1" + key = "mysql" + } +} +``` + +### MySQL instance + +This example shows how to create the single instance optionally managed by the module, providing all required attributes in the `test_instance` variable. The instance is purposefully kept simple and should only be used in development, or when designing infrastructures. + +```hcl +module "cos-mysql" { + source = "./modules/cos-container/mysql" + mysql_password = "foo" + test_instance = { + project_id = "my-project" + zone = "europe-west1-b" + name = "cos-mysql" + type = "n1-standard-1" + tags = ["ssh"] + metadata = {} + network = "default" + subnetwork = "https://www.googleapis.com/compute/v1/projects/my-project/regions/europe-west1/subnetworks/my-subnet" + disks = [] + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| mysql_password | MySQL root password. If an encrypted password is set, use the kms_config variable to specify KMS configuration. | string | ✓ | | +| *cloud_config* | Cloud config template path. If null default will be used. | string | | null | +| *config_variables* | Additional variables used to render the cloud-config template. | map(any) | | {} | +| *image* | MySQL container image. | string | | mysql:5.7 | +| *kms_config* | Optional KMS configuration to decrypt passed-in password. Leave null if a plaintext password is used. | object({...}) | | null | +| *mysql_config* | MySQL configuration file content, if null container default will be used. | string | | null | +| *mysql_data_disk* | MySQL data disk name in /dev/disk/by-id/ including the google- prefix. If null the boot disk will be used for data. | string | | null | +| *test_instance* | Test/development instance attributes, leave null to skip creation. | object({...}) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| cloud_config | Rendered cloud-config file to be passed as user-data instance metadata. | | +| test_instance | Optional test instance name and address | | + diff --git a/modules/cos-container/mysql/cloud-config.yaml b/modules/cos-container/mysql/cloud-config.yaml new file mode 100644 index 000000000..ba60bd099 --- /dev/null +++ b/modules/cos-container/mysql/cloud-config.yaml @@ -0,0 +1,117 @@ +#cloud-config + +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.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. + +users: + - name: mysql + uid: 2000 + +write_files: + - path: /var/lib/docker/daemon.json + permissions: 0644 + owner: root + content: | + { + "live-restore": true, + "storage-driver": "overlay2", + "log-opts": { + "max-size": "1024m" + } + } + - path: /run/mysql/secrets/mysql-passwd${kms_config == null ? "" : "-cipher"}.txt + permissions: 0600 + owner: root + content: | + ${password} + %{~ if kms_config != null ~} + - path: /run/mysql/passwd.sh + permissions: 0700 + owner: root + content: | + #!/bin/bash + base64 -d /run/mysql/secrets/mysql-passwd-cipher.txt | docker run \ + --rm -i -v /run/mysql/secrets:/data google/cloud-sdk:alpine \ + gcloud kms decrypt --ciphertext-file - \ + --plaintext-file /data/mysql-passwd.txt \ + --keyring ${kms_config.keyring} \ + --key ${kms_config.key} \ + --project ${kms_config.project_id} \ + --location ${kms_config.location} + %{~ endif ~} + %{~ if mysql_config != null ~} + - path: /run/mysql/etc/my.cnf + permissions: 0644 + owner: mysql + content: | + ${indent(6, mysql_config)} + %{~ endif ~} + %{~ if mysql_data_disk != null ~} + - path: /etc/systemd/system/mysql-data.service + permissions: 0644 + owner: root + content: | + [Unit] + Description=MySQL data disk + ConditionPathExists=/dev/disk/by-id/${mysql_data_disk} + Before=mysql.service + [Service] + Type=oneshot + ExecStart=/bin/mkdir -p /run/mysql/data + ExecStart=/bin/bash -c \ + "/bin/lsblk -fn -o FSTYPE \ + /dev/disk/by-id/${mysql_data_disk} |grep ext4 \ + || mkfs.ext4 -m 0 -F -E lazy_itable_init=0,lazy_journal_init=0,discard \ + /dev/disk/by-id/${mysql_data_disk}" + ExecStart=/bin/bash -c \ + "mount |grep /run/mysql/data \ + || mount -t ext4 /dev/disk/by-id/${mysql_data_disk} /run/mysql/data" + ExecStart=/sbin/resize2fs /dev/disk/by-id/${mysql_data_disk} + RemainAfterExit=true + %{~ endif ~} + - path: /etc/systemd/system/mysql.service + permissions: 0644 + owner: root + content: | + [Unit] + Description=MySQL service + After=%{~ if mysql_data_disk != null ~}mysql-data.service %{ endif ~}gcr-online.target docker.socket docker-events-collector.service + Wants=%{~ if mysql_data_disk != null ~}mysql-data.service %{ endif ~}gcr-online.target docker.socket + [Service] + %{~ if kms_config != null ~} + ExecStartPre=/run/mysql/passwd.sh + %{~ endif ~} + ExecStartPre=/bin/mkdir -p /run/mysql/data + ExecStartPre=/bin/chown -R 2000 /run/mysql/secrets /run/mysql/data + ExecStart=/usr/bin/docker run --rm --name=mysql \ + --user 2000:2000 \ + --log-driver=gcplogs \ + --network host \ + -e MYSQL_ROOT_PASSWORD_FILE=/etc/secrets/mysql-passwd.txt \ + -v /run/mysql/secrets:/etc/secrets \ + -v /run/mysql/data:/var/lib/mysql \ + %{~ if mysql_config != null ~} + -v /run/mysql/etc:/etc/mysql \ + %{~ endif ~} + ${image} \ + --ignore-db-dir=lost+found + ExecStop=/usr/bin/docker stop mysql + +bootcmd: + - systemctl start node-problem-detector + +runcmd: + - iptables -I INPUT 1 -p tcp -m tcp --dport 3306 -m state --state NEW,ESTABLISHED -j ACCEPT + - systemctl daemon-reload + - systemctl start mysql \ No newline at end of file diff --git a/modules/cos-container/mysql/instance.tf b/modules/cos-container/mysql/instance.tf new file mode 120000 index 000000000..bdef596b6 --- /dev/null +++ b/modules/cos-container/mysql/instance.tf @@ -0,0 +1 @@ +../instance.tf \ No newline at end of file diff --git a/modules/cos-container/mysql/main.tf b/modules/cos-container/mysql/main.tf new file mode 100644 index 000000000..dc352deac --- /dev/null +++ b/modules/cos-container/mysql/main.tf @@ -0,0 +1,30 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + cloud_config = templatefile(local.template, merge(var.config_variables, { + image = var.image + kms_config = var.kms_config + mysql_config = var.mysql_config + mysql_data_disk = var.mysql_data_disk + password = var.mysql_password + })) + template = ( + var.cloud_config == null + ? "${path.module}/cloud-config.yaml" + : var.cloud_config + ) +} diff --git a/modules/cos-container/mysql/outputs-instance.tf b/modules/cos-container/mysql/outputs-instance.tf new file mode 120000 index 000000000..ea9e24045 --- /dev/null +++ b/modules/cos-container/mysql/outputs-instance.tf @@ -0,0 +1 @@ +../outputs-instance.tf \ No newline at end of file diff --git a/modules/cos-container/mysql/outputs.tf b/modules/cos-container/mysql/outputs.tf new file mode 100644 index 000000000..205a55716 --- /dev/null +++ b/modules/cos-container/mysql/outputs.tf @@ -0,0 +1,20 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "cloud_config" { + description = "Rendered cloud-config file to be passed as user-data instance metadata." + value = local.cloud_config +} diff --git a/modules/cos-container/mysql/variables-instance.tf b/modules/cos-container/mysql/variables-instance.tf new file mode 120000 index 000000000..94af61e4d --- /dev/null +++ b/modules/cos-container/mysql/variables-instance.tf @@ -0,0 +1 @@ +../variables-instance.tf \ No newline at end of file diff --git a/modules/cos-container/mysql/variables.tf b/modules/cos-container/mysql/variables.tf new file mode 100644 index 000000000..3ec88259e --- /dev/null +++ b/modules/cos-container/mysql/variables.tf @@ -0,0 +1,61 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "cloud_config" { + description = "Cloud config template path. If null default will be used." + type = string + default = null +} + +variable "config_variables" { + description = "Additional variables used to render the cloud-config template." + type = map(any) + default = {} +} + +variable "image" { + description = "MySQL container image." + type = string + default = "mysql:5.7" +} + +variable "kms_config" { + description = "Optional KMS configuration to decrypt passed-in password. Leave null if a plaintext password is used." + type = object({ + project_id = string + keyring = string + location = string + key = string + }) + default = null +} + +variable "mysql_config" { + description = "MySQL configuration file content, if null container default will be used." + type = string + default = null +} + +variable "mysql_data_disk" { + description = "MySQL data disk name in /dev/disk/by-id/ including the google- prefix. If null the boot disk will be used for data." + type = string + default = null +} + +variable "mysql_password" { + description = "MySQL root password. If an encrypted password is set, use the kms_config variable to specify KMS configuration." + type = string +} diff --git a/modules/cos-container/outputs-instance.tf b/modules/cos-container/outputs-instance.tf new file mode 100644 index 000000000..0524baac2 --- /dev/null +++ b/modules/cos-container/outputs-instance.tf @@ -0,0 +1,24 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "test_instance" { + description = "Optional test instance name and address" + value = (var.test_instance == null ? {} : { + address = google_compute_instance.default[0].network_interface.0.network_ip + name = google_compute_instance.default[0].name + service_account = google_service_account.default[0].email + }) +} diff --git a/modules/cos-container/variables-instance.tf b/modules/cos-container/variables-instance.tf new file mode 100644 index 000000000..e8ee5415b --- /dev/null +++ b/modules/cos-container/variables-instance.tf @@ -0,0 +1,34 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "test_instance" { + description = "Test/development instance attributes, leave null to skip creation." + type = object({ + project_id = string + zone = string + name = string + type = string + tags = list(string) + metadata = map(string) + network = string + subnetwork = string + disks = map(object({ + read_only = bool + size = number + })) + }) + default = null +} diff --git a/modules/dns/README.md b/modules/dns/README.md new file mode 100644 index 000000000..6993e5695 --- /dev/null +++ b/modules/dns/README.md @@ -0,0 +1,48 @@ +# Google Cloud DNS Module + +This module allows simple management of Google Cloud DNS zones and records. It supports creating public, private, forwarding, and peering zones. For DNSSEC configuration, refer to the [`dns_managed_zone` documentation](https://www.terraform.io/docs/providers/google/r/dns_managed_zone.html#dnssec_config). + +## Example + +```hcl +module "private-dns" { + source = "./modules/dns" + project_id = "myproject" + type = "private" + name = "test-example" + domain = "test.example." + client_networks = [var.vpc_self_link] + recordsets = [ + { name = "localhost", type = "A", ttl = 300, records = ["127.0.0.1"] } + ] +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| domain | Zone domain, must end with a period. | string | ✓ | | +| name | Zone name, must be unique within the project. | string | ✓ | | +| project_id | Project id for the zone. | string | ✓ | | +| *client_networks* | List of VPC self links that can see this zone. | list(string) | | [] | +| *default_key_specs_key* | DNSSEC default key signing specifications: algorithm, key_length, key_type, kind. | any | | {} | +| *default_key_specs_zone* | DNSSEC default zone signing specifications: algorithm, key_length, key_type, kind. | any | | {} | +| *description* | Domain description. | string | | Terraform managed. | +| *dnssec_config* | DNSSEC configuration: kind, non_existence, state. | any | | {} | +| *forwarders* | List of target name servers, only valid for 'forwarding' zone types. | list(string) | | [] | +| *peer_network* | Peering network self link, only valid for 'peering' zone types. | string | | | +| *recordsets* | List of DNS record objects to manage. | list(object({...})) | | [] | +| *type* | Type of zone to create, valid values are 'public', 'private', 'forwarding', 'peering'. | string | | private | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| domain | The DNS zone domain. | | +| name | The DNS zone name. | | +| name_servers | The DNS zone name servers. | | +| type | The DNS zone type. | | +| zone | DNS zone resource. | | + diff --git a/modules/dns/main.tf b/modules/dns/main.tf new file mode 100644 index 000000000..4c9dfae21 --- /dev/null +++ b/modules/dns/main.tf @@ -0,0 +1,129 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + is_static_zone = var.type == "public" || var.type == "private" + recordsets = var.recordsets == null ? {} : { + for record in var.recordsets : + join("/", [record.name, record.type]) => record + } + zone = element(concat( + google_dns_managed_zone.non-public, google_dns_managed_zone.public + ), 0) +} + +resource "google_dns_managed_zone" "non-public" { + count = var.type != "public" ? 1 : 0 + provider = google-beta + project = var.project_id + name = var.name + dns_name = var.domain + description = "Terraform-managed zone." + visibility = "private" + + dynamic forwarding_config { + for_each = ( + var.type == "forwarding" && var.forwarders != null + ? { config = var.forwarders } + : {} + ) + iterator = config + content { + dynamic "target_name_servers" { + for_each = config.value + iterator = address + content { + ipv4_address = address.value + } + } + } + } + + dynamic peering_config { + for_each = ( + var.type == "peering" && var.peer_network != null + ? { config = var.peer_network } + : {} + ) + iterator = config + content { + target_network { + network_url = config.value + } + } + } + + private_visibility_config { + dynamic "networks" { + for_each = var.client_networks + iterator = network + content { + network_url = network.value + } + } + } + +} + +resource "google_dns_managed_zone" "public" { + count = var.type == "public" ? 1 : 0 + project = var.project_id + name = var.name + dns_name = var.domain + description = var.description + visibility = "public" + + dynamic "dnssec_config" { + for_each = var.dnssec_config == {} ? [] : list(var.dnssec_config) + iterator = config + content { + kind = lookup(config.value, "kind", "dns#managedZoneDnsSecConfig") + non_existence = lookup(config.value, "non_existence", "nsec3") + state = lookup(config.value, "state", "off") + + default_key_specs { + algorithm = lookup(var.default_key_specs_key, "algorithm", "rsasha256") + key_length = lookup(var.default_key_specs_key, "key_length", 2048) + key_type = lookup(var.default_key_specs_key, "key_type", "keySigning") + kind = lookup(var.default_key_specs_key, "kind", "dns#dnsKeySpec") + } + default_key_specs { + algorithm = lookup(var.default_key_specs_zone, "algorithm", "rsasha256") + key_length = lookup(var.default_key_specs_zone, "key_length", 1024) + key_type = lookup(var.default_key_specs_zone, "key_type", "zoneSigning") + kind = lookup(var.default_key_specs_zone, "kind", "dns#dnsKeySpec") + } + } + } + +} + +resource "google_dns_record_set" "cloud-static-records" { + for_each = ( + var.type == "public" || var.type == "private" + ? local.recordsets + : {} + ) + project = var.project_id + managed_zone = var.name + name = each.value.name != "" ? "${each.value.name}.${var.domain}" : var.domain + type = each.value.type + ttl = each.value.ttl + rrdatas = each.value.records + depends_on = [ + google_dns_managed_zone.non-public, google_dns_managed_zone.public + ] +} diff --git a/modules/dns/outputs.tf b/modules/dns/outputs.tf new file mode 100644 index 000000000..df35628a2 --- /dev/null +++ b/modules/dns/outputs.tf @@ -0,0 +1,40 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "type" { + description = "The DNS zone type." + value = var.type +} + +output "zone" { + description = "DNS zone resource." + value = local.zone +} + +output "name" { + description = "The DNS zone name." + value = local.zone.name +} + +output "domain" { + description = "The DNS zone domain." + value = local.zone.dns_name +} + +output "name_servers" { + description = "The DNS zone name servers." + value = local.zone.name_servers +} diff --git a/modules/dns/variables.tf b/modules/dns/variables.tf new file mode 100644 index 000000000..0991038c0 --- /dev/null +++ b/modules/dns/variables.tf @@ -0,0 +1,97 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +############################################################################### +# zone variables # +############################################################################### + +variable "client_networks" { + description = "List of VPC self links that can see this zone." + type = list(string) + default = [] +} + +variable "description" { + description = "Domain description." + type = string + default = "Terraform managed." +} + +# TODO(ludoo): add link to DNSSEC documentation in README +# https://www.terraform.io/docs/providers/google/r/dns_managed_zone.html#dnssec_config + +variable "default_key_specs_key" { + description = "DNSSEC default key signing specifications: algorithm, key_length, key_type, kind." + type = any + default = {} +} + +variable "default_key_specs_zone" { + description = "DNSSEC default zone signing specifications: algorithm, key_length, key_type, kind." + type = any + default = {} +} + +variable "dnssec_config" { + description = "DNSSEC configuration: kind, non_existence, state." + type = any + default = {} +} + +variable "domain" { + description = "Zone domain, must end with a period." + type = string +} + +# TODO(ludoo): add support for forwarding path attribute +variable "forwarders" { + description = "List of target name servers, only valid for 'forwarding' zone types." + type = list(string) + default = [] +} + +variable "name" { + description = "Zone name, must be unique within the project." + type = string +} + +variable "peer_network" { + description = "Peering network self link, only valid for 'peering' zone types." + type = string + default = "" +} + +variable "project_id" { + description = "Project id for the zone." + type = string +} + +variable "recordsets" { + type = list(object({ + name = string + type = string + ttl = number + records = list(string) + })) + description = "List of DNS record objects to manage." + default = [] +} + +variable "type" { + description = "Type of zone to create, valid values are 'public', 'private', 'forwarding', 'peering'." + type = string + default = "private" +} diff --git a/modules/dns/versions.tf b/modules/dns/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/dns/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/folders-unit/README.md b/modules/folders-unit/README.md new file mode 100644 index 000000000..fafc4c0fd --- /dev/null +++ b/modules/folders-unit/README.md @@ -0,0 +1,56 @@ +# Google Cloud Unit Folders Module + +This module allows creation and management of an organizational hierarchy "unit" composed of a parent folder (usually mapped to a business unit or team), and a set of child folders (usually mapped to environments) each with a corresponding set of service accounts, IAM bindings and GCS buckets. + +## Example + +```hcl +module "folders-unit" { + source = "./modules/folders-unit" + name = "Business Intelligence" + short_name = "bi" + automation_project_id = "automation-project-394yr923811" + billing_account_id = "015617-16GHBC-AF02D9" + organization_id = "506128240800" + root_node = "folders/93469270123701" + prefix = "unique-prefix" + environments = { + dev = "Development", + test = "Testing", + prod = "Production" + } + service_account_keys = true +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| automation_project_id | Project id used for automation service accounts. | string | ✓ | | +| billing_account_id | Country billing account account. | string | ✓ | | +| name | Top folder name. | string | ✓ | | +| organization_id | Organization id. | string | ✓ | | +| root_node | Root node in folders/folder_id or organizations/org_id format. | string | ✓ | | +| short_name | Short name used as GCS bucket and service account prefixes, do not use capital letters or spaces. | string | ✓ | | +| *environments* | Unit environments short names. | map(string) | | ... | +| *gcs_defaults* | Defaults use for the state GCS buckets. | map(string) | | ... | +| *iam_billing_config* | Grant billing user role to service accounts, defaults to granting on the billing account. | object({...}) | | ... | +| *iam_enviroment_roles* | IAM roles granted to the environment service account on the environment sub-folder. | list(string) | | ... | +| *iam_members* | IAM members for roles applied on the unit folder. | map(list(string)) | | null | +| *iam_roles* | IAM roles applied on the unit folder. | list(string) | | null | +| *iam_xpn_config* | Grant Shared VPC creation roles to service accounts, defaults to granting at folder level. | object({...}) | | ... | +| *prefix* | Optional prefix used for GCS bucket names to ensure uniqueness. | string | | null | +| *service_account_keys* | Generate and store service account keys in the state file. | bool | | false | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| env_folders | Unit environments folders. | | +| env_gcs_buckets | Unit environments tfstate gcs buckets. | | +| env_sa_keys | Unit environments service account keys. | ✓ | +| env_service_accounts | Unit environments service accounts. | | +| unit_folder | Unit top level folder. | | + diff --git a/modules/folders-unit/locals.tf b/modules/folders-unit/locals.tf new file mode 100644 index 000000000..1ad80700b --- /dev/null +++ b/modules/folders-unit/locals.tf @@ -0,0 +1,63 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + folder_roles = concat(var.iam_enviroment_roles, local.sa_xpn_folder_roles) + iam_members = var.iam_members == null ? {} : var.iam_members + iam_roles = var.iam_roles == null ? [] : var.iam_roles + unit_iam_bindings = { + for role in local.iam_roles : + role => lookup(local.iam_members, role, []) + } + folder_iam_service_account_bindings = { + for pair in setproduct(keys(var.environments), local.folder_roles) : + "${pair.0}-${pair.1}" => { environment = pair.0, role = pair.1 } + } + org_iam_service_account_bindings = { + for pair in setproduct(keys(var.environments), concat( + local.sa_xpn_org_roles, + local.sa_billing_org_roles, + local.sa_billing_org_roles)) : + "${pair.0}-${pair.1}" => { environment = pair.0, role = pair.1 } + } + billing_iam_service_account_bindings = { + for pair in setproduct(keys(var.environments), local.sa_billing_account_roles) : + "${pair.0}-${pair.1}" => { environment = pair.0, role = pair.1 } + } + service_accounts = { + for key, sa in google_service_account.environment : + key => "serviceAccount:${sa.email}" + } + sa_billing_account_roles = ( + var.iam_billing_config.target_org ? [] : ["roles/billing.user"] + ) + sa_billing_org_roles = ( + ! var.iam_billing_config.target_org ? [] : ["roles/billing.user"] + ) + sa_xpn_folder_roles = ( + local.sa_xpn_target_org ? [] : ["roles/compute.xpnAdmin"] + ) + sa_xpn_org_roles = ( + local.sa_xpn_target_org + ? ["roles/compute.xpnAdmin", "roles/resourcemanager.organizationViewer"] + : ["roles/resourcemanager.organizationViewer"] + ) + sa_xpn_target_org = ( + var.iam_xpn_config.target_org + || + substr(var.root_node, 0, 13) == "organizations" + ) +} diff --git a/modules/folders-unit/main.tf b/modules/folders-unit/main.tf new file mode 100644 index 000000000..33e68b131 --- /dev/null +++ b/modules/folders-unit/main.tf @@ -0,0 +1,109 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + organization_id = element(split("/", var.organization_id), 1) +} + +############################################################################### +# Folders and folder IAM # +############################################################################### + +resource "google_folder" "unit" { + display_name = var.name + parent = var.root_node +} + +resource "google_folder" "environment" { + for_each = var.environments + display_name = each.value + parent = google_folder.unit.name +} + +resource "google_folder_iam_binding" "unit" { + for_each = local.unit_iam_bindings + folder = google_folder.unit.name + role = each.key + members = each.value +} + +resource "google_folder_iam_binding" "environment" { + for_each = local.folder_iam_service_account_bindings + folder = google_folder.environment[each.value.environment].name + role = each.value.role + members = [local.service_accounts[each.value.environment]] +} + +############################################################################### +# Billing account and org IAM # +############################################################################### + +resource "google_organization_iam_member" "org_iam_member" { + for_each = local.org_iam_service_account_bindings + org_id = local.organization_id + role = each.value.role + member = local.service_accounts[each.value.environment] +} + +resource "google_billing_account_iam_member" "billing_iam_member" { + for_each = local.billing_iam_service_account_bindings + billing_account_id = var.billing_account_id + role = each.value.role + member = local.service_accounts[each.value.environment] +} + +################################################################################ +# Service Accounts # +################################################################################ + +resource "google_service_account" "environment" { + for_each = var.environments + project = var.automation_project_id + account_id = "${var.short_name}-${each.key}" + display_name = "${var.short_name} ${each.key} (Terraform managed)." +} + +resource "google_service_account_key" "keys" { + for_each = var.service_account_keys ? var.environments : {} + service_account_id = google_service_account.environment[each.key].email +} + +################################################################################ +# GCS and GCS IAM # +################################################################################ + +resource "google_storage_bucket" "tfstate" { + for_each = var.environments + project = var.automation_project_id + name = join("", [ + var.prefix == null ? "" : "${var.prefix}-", + "${var.short_name}-${each.key}-tf" + ]) + location = var.gcs_defaults.location + storage_class = var.gcs_defaults.storage_class + force_destroy = false + bucket_policy_only = true + versioning { + enabled = true + } +} + +resource "google_storage_bucket_iam_binding" "bindings" { + for_each = var.environments + bucket = google_storage_bucket.tfstate[each.key].name + role = "roles/storage.objectAdmin" + members = [local.service_accounts[each.key]] +} diff --git a/modules/folders-unit/outputs.tf b/modules/folders-unit/outputs.tf new file mode 100644 index 000000000..520712f50 --- /dev/null +++ b/modules/folders-unit/outputs.tf @@ -0,0 +1,59 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "unit_folder" { + description = "Unit top level folder." + value = { + id = google_folder.unit.name, + name = google_folder.unit.display_name + } +} + +output "env_gcs_buckets" { + description = "Unit environments tfstate gcs buckets." + value = { + for key, bucket in google_storage_bucket.tfstate + : key => bucket.name + } +} + +output "env_folders" { + description = "Unit environments folders." + value = { + for key, folder in google_folder.environment + : key => { + id = folder.name, + name = folder.display_name + } + } +} + +output "env_service_accounts" { + description = "Unit environments service accounts." + value = { + for key, sa in google_service_account.environment + : key => sa.email + } +} + +output "env_sa_keys" { + description = "Unit environments service account keys." + sensitive = true + value = { + for key, sa_key in google_service_account_key.keys : + key => sa_key.private_key + } +} diff --git a/modules/folders-unit/variables.tf b/modules/folders-unit/variables.tf new file mode 100644 index 000000000..f7a8df3fa --- /dev/null +++ b/modules/folders-unit/variables.tf @@ -0,0 +1,122 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "automation_project_id" { + description = "Project id used for automation service accounts." + type = string +} + +variable "billing_account_id" { + description = "Country billing account account." + type = string +} + +variable "environments" { + description = "Unit environments short names." + type = map(string) + default = { + non-prod = "Non production" + prod = "Production" + } +} + +variable "gcs_defaults" { + description = "Defaults use for the state GCS buckets." + type = map(string) + default = { + location = "EU" + storage_class = "MULTI_REGIONAL" + } +} + +variable "iam_billing_config" { + description = "Grant billing user role to service accounts, defaults to granting on the billing account." + type = object({ + grant = bool + target_org = bool + }) + default = { + grant = true + target_org = false + } +} + +variable "iam_enviroment_roles" { + description = "IAM roles granted to the environment service account on the environment sub-folder." + type = list(string) + default = [ + "roles/compute.networkAdmin", + "roles/owner", + "roles/resourcemanager.folderAdmin", + "roles/resourcemanager.projectCreator", + ] +} + +variable "iam_members" { + description = "IAM members for roles applied on the unit folder." + type = map(list(string)) + default = null +} + +variable "iam_roles" { + description = "IAM roles applied on the unit folder." + type = list(string) + default = null +} + +variable "iam_xpn_config" { + description = "Grant Shared VPC creation roles to service accounts, defaults to granting at folder level." + type = object({ + grant = bool + target_org = bool + }) + default = { + grant = true + target_org = false + } +} + +variable "name" { + description = "Top folder name." + type = string +} + +variable "organization_id" { + description = "Organization id in organizations/nnnnnn format." + type = string +} + +variable "prefix" { + description = "Optional prefix used for GCS bucket names to ensure uniqueness." + type = string + default = null +} + +variable "root_node" { + description = "Root node in folders/folder_id or organizations/org_id format." + type = string +} + +variable "service_account_keys" { + description = "Generate and store service account keys in the state file." + type = bool + default = false +} + +variable "short_name" { + description = "Short name used as GCS bucket and service account prefixes, do not use capital letters or spaces." + type = string +} diff --git a/modules/folders-unit/versions.tf b/modules/folders-unit/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/folders-unit/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/folders/README.md b/modules/folders/README.md new file mode 100644 index 000000000..e12947a45 --- /dev/null +++ b/modules/folders/README.md @@ -0,0 +1,45 @@ +# Google Cloud Folder Module + +This module allow creation and management of sets of folders sharing a common parent, and their individual IAM bindings. + +## Example + +```hcl +module "folder" { + source = "./modules/folder" + parent = "organizations/1234567890" + names = ["Folder one", "Folder two] + iam_members = { + "Folder one" = { + "roles/owner" => ["group:users@example.com"] + } + } + iam_roles = { + "Folder one" = ["roles/owner"] + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| parent | Parent in folders/folder_id or organizations/org_id format. | string | ✓ | | +| *iam_members* | List of IAM members keyed by folder name and role. | map(map(list(string))) | | null | +| *iam_roles* | List of IAM roles keyed by folder name. | map(list(string)) | | null | +| *names* | Folder names. | list(string) | | [] | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| folder | Folder resource (for single use). | | +| folders | Folder resources. | | +| id | Folder id (for single use). | | +| ids | Folder ids. | | +| ids_list | List of folder ids. | | +| name | Folder name (for single use). | | +| names | Folder names. | | +| names_list | List of folder names. | | + diff --git a/modules/folders/main.tf b/modules/folders/main.tf new file mode 100644 index 000000000..228a195bf --- /dev/null +++ b/modules/folders/main.tf @@ -0,0 +1,50 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + folders = ( + local.has_folders + ? [for name in var.names : google_folder.folders[name]] + : [] + ) + # needed when destroying + has_folders = length(google_folder.folders) > 0 + iam_pairs = var.iam_roles == null ? [] : flatten([ + for name, roles in var.iam_roles : + [for role in roles : { name = name, role = role }] + ]) + iam_keypairs = { + for pair in local.iam_pairs : + "${pair.name}-${pair.role}" => pair + } + iam_members = var.iam_members == null ? {} : var.iam_members +} + +resource "google_folder" "folders" { + for_each = toset(var.names) + display_name = each.value + parent = var.parent +} + +resource "google_folder_iam_binding" "authoritative" { + for_each = local.iam_keypairs + folder = google_folder.folders[each.value.name].name + role = each.value.role + members = lookup( + lookup(local.iam_members, each.value.name, {}), each.value.role, [] + ) +} + diff --git a/modules/folders/outputs.tf b/modules/folders/outputs.tf new file mode 100644 index 000000000..c7d061500 --- /dev/null +++ b/modules/folders/outputs.tf @@ -0,0 +1,63 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "folder" { + description = "Folder resource (for single use)." + value = local.has_folders ? local.folders[0] : null +} + +output "id" { + description = "Folder id (for single use)." + value = local.has_folders ? local.folders[0].name : null +} + +output "name" { + description = "Folder name (for single use)." + value = local.has_folders ? local.folders[0].display_name : null +} + +output "folders" { + description = "Folder resources." + value = local.folders +} + +output "ids" { + description = "Folder ids." + value = ( + local.has_folders + ? zipmap(var.names, [for f in local.folders : f.name]) + : {} + ) +} + +output "names" { + description = "Folder names." + value = ( + local.has_folders + ? zipmap(var.names, [for f in local.folders : f.display_name]) + : {} + ) +} + +output "ids_list" { + description = "List of folder ids." + value = [for f in local.folders : f.name] +} + +output "names_list" { + description = "List of folder names." + value = [for f in local.folders : f.display_name] +} diff --git a/modules/folders/variables.tf b/modules/folders/variables.tf new file mode 100644 index 000000000..742031633 --- /dev/null +++ b/modules/folders/variables.tf @@ -0,0 +1,38 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "iam_members" { + description = "List of IAM members keyed by folder name and role." + type = map(map(list(string))) + default = null +} + +variable "iam_roles" { + description = "List of IAM roles keyed by folder name." + type = map(list(string)) + default = null +} + +variable "names" { + description = "Folder names." + type = list(string) + default = [] +} + +variable "parent" { + description = "Parent in folders/folder_id or organizations/org_id format." + type = string +} diff --git a/modules/folders/versions.tf b/modules/folders/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/folders/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/gcs/README.md b/modules/gcs/README.md new file mode 100644 index 000000000..2058365b8 --- /dev/null +++ b/modules/gcs/README.md @@ -0,0 +1,54 @@ +# Google Cloud Storage Module + +## Example + +```hcl +module "buckets" { + source = "./modules/gcs" + project_id = "myproject" + prefix = "test" + names = ["bucket-one", "bucket-two"] + bucket_policy_only = { + bucket-one = false + } + iam_members = { + bucket-two = { + "roles/storage.admin" = ["group:storage@example.com"] + } + } + iam_roles = { + bucket-two = ["roles/storage.admin"] + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| names | Bucket name suffixes. | list(string) | ✓ | | +| project_id | Bucket project id. | string | ✓ | | +| *bucket_policy_only* | Optional map to disable object ACLS keyed by name, defaults to true. | map(bool) | | {} | +| *force_destroy* | Optional map to set force destroy keyed by name, defaults to false. | map(bool) | | {} | +| *iam_members* | IAM members keyed by bucket name and role. | map(map(list(string))) | | null | +| *iam_roles* | IAM roles keyed by bucket name. | map(list(string)) | | null | +| *labels* | Labels to be attached to all buckets. | map(string) | | {} | +| *location* | Bucket location. | string | | EU | +| *prefix* | Prefix used to generate the bucket name. | string | | | +| *storage_class* | Bucket storage class. | string | | MULTI_REGIONAL | +| *versioning* | Optional map to set versioning keyed by name, defaults to false. | map(bool) | | {} | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| bucket | Bucket resource (for single use). | | +| buckets | Bucket resources. | | +| name | Bucket name (for single use). | | +| names | Bucket names. | | +| names_list | List of bucket names. | | +| url | Bucket URL (for single use). | | +| urls | Bucket URLs. | | +| urls_list | List of bucket URLs. | | + diff --git a/modules/gcs/main.tf b/modules/gcs/main.tf new file mode 100644 index 000000000..baff11d9e --- /dev/null +++ b/modules/gcs/main.tf @@ -0,0 +1,62 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + buckets = ( + local.has_buckets + ? [for name in var.names : google_storage_bucket.buckets[name]] + : [] + ) + # needed when destroying + has_buckets = length(google_storage_bucket.buckets) > 0 + iam_pairs = var.iam_roles == null ? [] : flatten([ + for name, roles in var.iam_roles : + [for role in roles : { name = name, role = role }] + ]) + iam_keypairs = { + for pair in local.iam_pairs : + "${pair.name}-${pair.role}" => pair + } + iam_members = var.iam_members == null ? {} : var.iam_members + prefix = var.prefix == "" ? "" : join("-", [var.prefix, lower(var.location), ""]) +} + +resource "google_storage_bucket" "buckets" { + for_each = toset(var.names) + name = "${local.prefix}${lower(each.key)}" + project = var.project_id + location = var.location + storage_class = var.storage_class + force_destroy = lookup(var.force_destroy, each.key, false) + bucket_policy_only = lookup(var.bucket_policy_only, each.key, true) + versioning { + enabled = lookup(var.versioning, each.key, false) + } + labels = merge(var.labels, { + location = lower(var.location) + name = lower(each.key) + storage_class = lower(var.storage_class) + }) +} + +resource "google_storage_bucket_iam_binding" "bindings" { + for_each = local.iam_keypairs + bucket = google_storage_bucket.buckets[each.value.name].name + role = each.value.role + members = lookup( + lookup(local.iam_members, each.value.name, {}), each.value.role, [] + ) +} diff --git a/modules/gcs/outputs.tf b/modules/gcs/outputs.tf new file mode 100644 index 000000000..9a8f8dfae --- /dev/null +++ b/modules/gcs/outputs.tf @@ -0,0 +1,63 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "bucket" { + description = "Bucket resource (for single use)." + value = local.has_buckets ? local.buckets[0] : null +} + +output "name" { + description = "Bucket name (for single use)." + value = local.has_buckets ? local.buckets[0].name : null +} + +output "url" { + description = "Bucket URL (for single use)." + value = local.has_buckets ? local.buckets[0].url : null +} + +output "buckets" { + description = "Bucket resources." + value = local.buckets +} + +output "names" { + description = "Bucket names." + value = ( + local.has_buckets + ? zipmap(var.names, [for b in local.buckets : lookup(b, "name", null)]) + : {} + ) +} + +output "urls" { + description = "Bucket URLs." + value = ( + local.has_buckets + ? zipmap(var.names, [for b in local.buckets : b.url]) + : {} + ) +} + +output "names_list" { + description = "List of bucket names." + value = [for b in local.buckets : b.name] +} + +output "urls_list" { + description = "List of bucket URLs." + value = [for b in local.buckets : b.name] +} diff --git a/modules/gcs/variables.tf b/modules/gcs/variables.tf new file mode 100644 index 000000000..6cc712ebc --- /dev/null +++ b/modules/gcs/variables.tf @@ -0,0 +1,79 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "bucket_policy_only" { + description = "Optional map to disable object ACLS keyed by name, defaults to true." + type = map(bool) + default = {} +} + +variable "force_destroy" { + description = "Optional map to set force destroy keyed by name, defaults to false." + type = map(bool) + default = {} +} + +variable "iam_members" { + description = "IAM members keyed by bucket name and role." + type = map(map(list(string))) + default = null +} + +variable "iam_roles" { + description = "IAM roles keyed by bucket name." + type = map(list(string)) + default = null +} + +variable "labels" { + description = "Labels to be attached to all buckets." + type = map(string) + default = {} +} + +variable "location" { + description = "Bucket location." + type = string + default = "EU" +} + +variable "names" { + description = "Bucket name suffixes." + type = list(string) +} + +variable "prefix" { + description = "Prefix used to generate the bucket name." + type = string + default = "" +} + +variable "project_id" { + description = "Bucket project id." + type = string +} + +variable "storage_class" { + description = "Bucket storage class." + type = string + default = "MULTI_REGIONAL" +} + +variable "versioning" { + description = "Optional map to set versioning keyed by name, defaults to false." + type = map(bool) + default = {} +} diff --git a/modules/gcs/versions.tf b/modules/gcs/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/gcs/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/gke-cluster/README.md b/modules/gke-cluster/README.md new file mode 100644 index 000000000..fb0c95afb --- /dev/null +++ b/modules/gke-cluster/README.md @@ -0,0 +1,77 @@ +# GKE cluster module + +This module allows simplified creation and management of GKE clusters and should be used together with the GKE nodepool module, as the default nodepool is turned off here and cannot be re-enabled. Some sensible defaults are set initially, in order to allow less verbose usage for most use cases. + +## Example + +```hcl +module "cluster-1" { + source = "./modules/gke-cluster" + project_id = "myproject" + name = "cluster-1" + location = "europe-west1-b" + network = var.network_self_link + subnetwork = var.subnet_self_link + secondary_range_pods = "pods" + secondary_range_services = "services" + default_max_pods_per_node = 32 + master_authorized_ranges = { + internal-vms = "10.0.0.0/8" + } + private_cluster_config = { + enable_private_nodes = true + enable_private_endpoint = true + master_ipv4_cidr_block = "192.168.0.0/28" + } + labels = { + environment = "dev" + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| location | Cluster zone or region. | string | ✓ | | +| name | Cluster name. | string | ✓ | | +| network | Name or self link of the VPC used for the cluster. Use the self link for Shared VPC. | string | ✓ | | +| project_id | Cluster project id. | string | ✓ | | +| secondary_range_pods | Subnet secondary range name used for pods. | string | ✓ | | +| secondary_range_services | Subnet secondary range name used for services. | string | ✓ | | +| subnetwork | VPC subnetwork name or self link. | string | ✓ | | +| *addons* | Addons enabled in the cluster (true means enabled). | object({...}) | | ... | +| *authenticator_security_group* | RBAC security group for Google Groups for GKE, format is gke-security-groups@yourdomain.com. | string | | null | +| *cluster_autoscaling* | Enable and configure limits for Node Auto-Provisioning with Cluster Autoscaler. | object({...}) | | ... | +| *database_encryption* | Enable and configure GKE application-layer secrets encryption. | object({...}) | | ... | +| *default_max_pods_per_node* | Maximum number of pods per node in this cluster. | number | | 110 | +| *description* | Cluster description. | string | | null | +| *enable_binary_authorization* | Enable Google Binary Authorization. | bool | | null | +| *enable_intranode_visibility* | Enable intra-node visibility to make same node pod to pod traffic visible. | bool | | null | +| *enable_shielded_nodes* | Enable Shielded Nodes features on all nodes in this cluster. | bool | | null | +| *enable_tpu* | Enable Cloud TPU resources in this cluster. | bool | | null | +| *labels* | Cluster resource labels. | map(string) | | null | +| *logging_service* | Logging service (disable with an empty string). | string | | logging.googleapis.com/kubernetes | +| *maintenance_start_time* | Maintenance start time in RFC3339 format 'HH:MM', where HH is [00-23] and MM is [00-59] GMT. | string | | 03:00 | +| *master_authorized_ranges* | External Ip address ranges that can access the Kubernetes cluster master through HTTPS. | map(string) | | {} | +| *min_master_version* | Minimum version of the master, defaults to the version of the most recent official release. | string | | null | +| *monitoring_service* | Monitoring service (disable with an empty string). | string | | monitoring.googleapis.com/kubernetes | +| *node_locations* | Zones in which the cluster's nodes are located. | list(string) | | [] | +| *pod_security_policy* | Enable the PodSecurityPolicy feature. | bool | | null | +| *private_cluster_config* | Enable and configure private cluster. | object({...}) | | null | +| *release_channel* | Release channel for GKE upgrades. | string | | null | +| *resource_usage_export_config* | Configure the ResourceUsageExportConfig feature. | object({...}) | | ... | +| *vertical_pod_autoscaling* | Enable the Vertical Pod Autoscaling feature. | bool | | null | +| *workload_identity* | Enable the Workload Identity feature. | bool | | true | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| cluster | Cluster resource. | ✓ | +| endpoint | Cluster endpoint. | | +| location | Cluster location. | | +| master_version | Master version. | | +| name | Cluster name. | | + diff --git a/modules/gke-cluster/main.tf b/modules/gke-cluster/main.tf new file mode 100644 index 000000000..67e95dd3c --- /dev/null +++ b/modules/gke-cluster/main.tf @@ -0,0 +1,197 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_container_cluster" "cluster" { + provider = google-beta + project = var.project_id + name = var.name + description = var.description + location = var.location + node_locations = length(var.node_locations) == 0 ? null : var.node_locations + min_master_version = var.min_master_version + network = var.network + subnetwork = var.subnetwork + logging_service = var.logging_service + monitoring_service = var.monitoring_service + resource_labels = var.labels + default_max_pods_per_node = var.default_max_pods_per_node + enable_binary_authorization = var.enable_binary_authorization + enable_intranode_visibility = var.enable_intranode_visibility + enable_shielded_nodes = var.enable_shielded_nodes + enable_tpu = var.enable_tpu + initial_node_count = 1 + remove_default_node_pool = true + + # node_config + + addons_config { + http_load_balancing { + disabled = ! var.addons.http_load_balancing + } + horizontal_pod_autoscaling { + disabled = ! var.addons.horizontal_pod_autoscaling + } + network_policy_config { + disabled = ! var.addons.network_policy_config + } + # beta addons + # cloudrun is dynamic as it tends to trigger cluster recreation on change + dynamic cloudrun_config { + for_each = var.addons.istio_config.enabled && var.addons.cloudrun_config ? [""] : [] + content { + disabled = false + } + } + istio_config { + disabled = ! var.addons.istio_config.enabled + auth = var.addons.istio_config.tls ? "AUTH_MUTUAL_TLS" : "AUTH_NONE" + } + } + + # TODO(ludomagno): support setting address ranges instead of range names + # https://www.terraform.io/docs/providers/google/r/container_cluster.html#cluster_ipv4_cidr_block + ip_allocation_policy { + cluster_secondary_range_name = var.secondary_range_pods + services_secondary_range_name = var.secondary_range_services + } + + # TODO(ludomagno): make optional, and support beta feature + # https://www.terraform.io/docs/providers/google/r/container_cluster.html#daily_maintenance_window + maintenance_policy { + daily_maintenance_window { + start_time = var.maintenance_start_time + } + } + + master_auth { + client_certificate_config { + issue_client_certificate = false + } + } + + dynamic master_authorized_networks_config { + for_each = length(var.master_authorized_ranges) == 0 ? [] : list(var.master_authorized_ranges) + iterator = ranges + content { + dynamic cidr_blocks { + for_each = ranges.value + iterator = range + content { + cidr_block = range.value + display_name = range.key + } + } + } + } + + dynamic network_policy { + for_each = var.addons.network_policy_config ? [""] : [] + content { + enabled = true + provider = "CALICO" + } + } + + dynamic private_cluster_config { + for_each = var.private_cluster_config != null ? [var.private_cluster_config] : [] + iterator = config + content { + enable_private_nodes = config.value.enable_private_nodes + enable_private_endpoint = config.value.enable_private_endpoint + master_ipv4_cidr_block = config.value.master_ipv4_cidr_block + } + } + + # beta features + + dynamic authenticator_groups_config { + for_each = var.authenticator_security_group == null ? [] : [""] + content { + security_group = var.authenticator_security_group + } + } + + dynamic cluster_autoscaling { + for_each = var.cluster_autoscaling.enabled ? [var.cluster_autoscaling] : [] + iterator = config + content { + enabled = true + resource_limits { + resource_type = "cpu" + minimum = config.cpu_min + maximum = config.cpu_max + } + resource_limits { + resource_type = "memory" + minimum = config.memory_min + maximum = config.memory_max + } + } + } + + dynamic database_encryption { + for_each = var.database_encryption.enabled ? [var.database_encryption] : [] + iterator = config + content { + state = config.value.state + key_name = config.value.key_name + } + } + + dynamic pod_security_policy_config { + for_each = var.pod_security_policy != null ? [""] : [] + content { + enabled = var.pod_security_policy + } + } + + dynamic release_channel { + for_each = var.release_channel != null ? [""] : [] + content { + channel = var.release_channel + } + } + + dynamic resource_usage_export_config { + for_each = ( + var.resource_usage_export_config.enabled != null + && + var.resource_usage_export_config.dataset != null + ? [""] : [] + ) + content { + enable_network_egress_metering = var.resource_usage_export_config.enabled + bigquery_destination { + dataset_id = var.resource_usage_export_config.dataset + } + } + } + + dynamic vertical_pod_autoscaling { + for_each = var.vertical_pod_autoscaling == null ? [] : [""] + content { + enabled = var.vertical_pod_autoscaling + } + } + + dynamic workload_identity_config { + for_each = var.workload_identity ? [""] : [] + content { + identity_namespace = "${var.project_id}.svc.id.goog" + } + } + +} diff --git a/modules/gke-cluster/outputs.tf b/modules/gke-cluster/outputs.tf new file mode 100644 index 000000000..6937ee966 --- /dev/null +++ b/modules/gke-cluster/outputs.tf @@ -0,0 +1,41 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 resource." + sensitive = true + value = google_container_cluster.cluster +} + +output "endpoint" { + description = "Cluster endpoint." + value = google_container_cluster.cluster.endpoint +} + +output "location" { + description = "Cluster location." + value = google_container_cluster.cluster.location +} + +output "master_version" { + description = "Master version." + value = google_container_cluster.cluster.master_version +} + +output "name" { + description = "Cluster name." + value = google_container_cluster.cluster.name +} diff --git a/modules/gke-cluster/variables.tf b/modules/gke-cluster/variables.tf new file mode 100644 index 000000000..e39667711 --- /dev/null +++ b/modules/gke-cluster/variables.tf @@ -0,0 +1,236 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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" { + description = "Addons enabled in the cluster (true means enabled)." + type = object({ + horizontal_pod_autoscaling = bool + http_load_balancing = bool + network_policy_config = bool + cloudrun_config = bool + istio_config = object({ + enabled = bool + tls = bool + }) + }) + default = { + horizontal_pod_autoscaling = true + http_load_balancing = true + network_policy_config = false + cloudrun_config = false + istio_config = { + enabled = false + tls = false + } + } +} + +variable "authenticator_security_group" { + description = "RBAC security group for Google Groups for GKE, format is gke-security-groups@yourdomain.com." + type = string + default = null +} + +variable "cluster_autoscaling" { + description = "Enable and configure limits for Node Auto-Provisioning with Cluster Autoscaler." + type = object({ + enabled = bool + cpu_min = number + cpu_max = number + memory_min = number + memory_max = number + }) + default = { + enabled = false + cpu_min = 0 + cpu_max = 0 + memory_min = 0 + memory_max = 0 + } +} + +variable "database_encryption" { + description = "Enable and configure GKE application-layer secrets encryption." + type = object({ + enabled = bool + state = string + key_name = string + }) + default = { + enabled = false + state = "DECRYPTED" + key_name = null + } +} + +variable "default_max_pods_per_node" { + description = "Maximum number of pods per node in this cluster." + type = number + default = 110 +} + +variable "description" { + description = "Cluster description." + type = string + default = null +} + +variable "enable_binary_authorization" { + description = "Enable Google Binary Authorization." + type = bool + default = null +} + +variable "enable_intranode_visibility" { + description = "Enable intra-node visibility to make same node pod to pod traffic visible." + type = bool + default = null +} + +variable "enable_shielded_nodes" { + description = "Enable Shielded Nodes features on all nodes in this cluster." + type = bool + default = null +} + +variable "enable_tpu" { + description = "Enable Cloud TPU resources in this cluster." + type = bool + default = null +} + +variable "labels" { + description = "Cluster resource labels." + type = map(string) + default = null +} + +variable "location" { + description = "Cluster zone or region." + type = string +} + +variable "logging_service" { + description = "Logging service (disable with an empty string)." + type = string + default = "logging.googleapis.com/kubernetes" +} + +variable "maintenance_start_time" { + description = "Maintenance start time in RFC3339 format 'HH:MM', where HH is [00-23] and MM is [00-59] GMT." + type = string + default = "03:00" +} + +variable "master_authorized_ranges" { + description = "External Ip address ranges that can access the Kubernetes cluster master through HTTPS." + type = map(string) + default = {} +} + +variable "min_master_version" { + description = "Minimum version of the master, defaults to the version of the most recent official release." + type = string + default = null +} + +variable "monitoring_service" { + description = "Monitoring service (disable with an empty string)." + type = string + default = "monitoring.googleapis.com/kubernetes" +} + +variable "name" { + description = "Cluster name." + type = string +} + +variable "network" { + description = "Name or self link of the VPC used for the cluster. Use the self link for Shared VPC." + type = string +} + +variable "node_locations" { + description = "Zones in which the cluster's nodes are located." + type = list(string) + default = [] +} + +variable "pod_security_policy" { + description = "Enable the PodSecurityPolicy feature." + type = bool + default = null +} + +variable "private_cluster_config" { + description = "Enable and configure private cluster." + type = object({ + enable_private_nodes = bool + enable_private_endpoint = bool + master_ipv4_cidr_block = string + }) + default = null +} + +variable "project_id" { + description = "Cluster project id." + type = string +} + +variable "release_channel" { + description = "Release channel for GKE upgrades." + type = string + default = null +} + +variable "resource_usage_export_config" { + description = "Configure the ResourceUsageExportConfig feature." + type = object({ + enabled = bool + dataset = string + }) + default = { + enabled = null + dataset = null + } +} + +variable "secondary_range_pods" { + description = "Subnet secondary range name used for pods." + type = string +} + +variable "secondary_range_services" { + description = "Subnet secondary range name used for services." + type = string +} + +variable "subnetwork" { + description = "VPC subnetwork name or self link." + type = string +} + +variable "vertical_pod_autoscaling" { + description = "Enable the Vertical Pod Autoscaling feature." + type = bool + default = null +} + +variable "workload_identity" { + description = "Enable the Workload Identity feature." + type = bool + default = true +} diff --git a/modules/gke-cluster/versions.tf b/modules/gke-cluster/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/gke-cluster/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/gke-nodepool/README.md b/modules/gke-nodepool/README.md new file mode 100644 index 000000000..fcaa4bae5 --- /dev/null +++ b/modules/gke-nodepool/README.md @@ -0,0 +1,56 @@ +# GKE nodepool module + +This module allows simplified creation and management of individual GKE nodepools, setting sensible defaults (eg a service account is created for nodes if none is set) and allowing for less verbose usage in most use cases. + +## Example usage + +```hcl +module "cluster-1-nodepool-1" { + source = "../modules/gke-nodepool" + project_id = "myproject" + cluster_name = "cluster-1" + location = "europe-west1-b" + name = "nodepool-1" +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| cluster_name | Cluster name. | string | ✓ | | +| location | Cluster location. | string | ✓ | | +| project_id | Cluster project id. | string | ✓ | | +| *autoscaling_config* | Optional autoscaling configuration. | object({...}) | | null | +| *gke_version* | Kubernetes nodes version. Ignored if auto_upgrade is set in management_config. | string | | null | +| *initial_node_count* | Initial number of nodes for the pool. | number | | 1 | +| *management_config* | Optional node management configuration. | object({...}) | | null | +| *max_pods_per_node* | Maximum number of pods per node. | number | | null | +| *name* | Optional nodepool name. | string | | null | +| *node_config_disk_size* | Node disk size, defaults to 100GB. | number | | 100 | +| *node_config_disk_type* | Node disk type, defaults to pd-standard. | string | | pd-standard | +| *node_config_guest_accelerator* | Map of type and count of attached accelerator cards. | map(number) | | {} | +| *node_config_image_type* | Nodes image type. | string | | null | +| *node_config_labels* | Kubernetes labels attached to nodes. | map(string) | | {} | +| *node_config_local_ssd_count* | Number of local SSDs attached to nodes. | number | | 0 | +| *node_config_machine_type* | Nodes machine type. | string | | n1-standard-1 | +| *node_config_metadata* | Metadata key/value pairs assigned to nodes. Set disable-legacy-endpoints to true when using this variable. | map(string) | | null | +| *node_config_min_cpu_platform* | Minimum CPU platform for nodes. | string | | null | +| *node_config_oauth_scopes* | Set of Google API scopes for the nodes service account. Include logging-write, monitoring, and storage-ro when using this variable. | list(string) | | ["logging-write", "monitoring", "monitoring-write", "storage-ro"] | +| *node_config_preemptible* | Use preemptible VMs for nodes. | bool | | null | +| *node_config_sandbox_config* | GKE Sandbox configuration. Needs image_type set to COS_CONTAINERD and node_version set to 1.12.7-gke.17 when using this variable. | string | | null | +| *node_config_service_account* | Service account used for nodes. | string | | null | +| *node_config_shielded_instance_config* | Shielded instance options. | object({...}) | | null | +| *node_config_tags* | Network tags applied to nodes. | list(string) | | null | +| *node_config_workload_metadata_config* | Metadata configuration to expose to workloads on the node pool. | string | | SECURE | +| *node_count* | Number of nodes per instance group, can be updated after creation. Ignored when autoscaling is set. | number | | null | +| *node_locations* | Optional list of zones in which nodes should be located. Uses cluster locations if unset. | list(string) | | null | +| *upgrade_config* | Optional node upgrade configuration. | object({...}) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| name | Nodepool name. | | + diff --git a/modules/gke-nodepool/main.tf b/modules/gke-nodepool/main.tf new file mode 100644 index 000000000..40019e09c --- /dev/null +++ b/modules/gke-nodepool/main.tf @@ -0,0 +1,111 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_container_node_pool" "nodepool" { + provider = google-beta + + project = var.project_id + cluster = var.cluster_name + location = var.location + name = var.name + + initial_node_count = var.initial_node_count + max_pods_per_node = var.max_pods_per_node + node_count = var.autoscaling_config == null ? var.node_count : null + node_locations = var.node_locations + version = var.gke_version + + node_config { + disk_size_gb = var.node_config_disk_size + disk_type = var.node_config_disk_type + image_type = var.node_config_image_type + labels = var.node_config_labels + local_ssd_count = var.node_config_local_ssd_count + machine_type = var.node_config_machine_type + metadata = var.node_config_metadata + min_cpu_platform = var.node_config_min_cpu_platform + oauth_scopes = var.node_config_oauth_scopes + preemptible = var.node_config_preemptible + service_account = var.node_config_service_account + tags = var.node_config_tags + + dynamic guest_accelerator { + for_each = var.node_config_guest_accelerator + iterator = config + content { + type = config.key + count = config.value + } + } + + dynamic sandbox_config { + for_each = ( + var.node_config_sandbox_config != null + ? [var.node_config_sandbox_config] + : [] + ) + iterator = config + content { + sandbox_type = config.value + } + } + + dynamic shielded_instance_config { + for_each = ( + var.node_config_shielded_instance_config != null + ? [var.node_config_shielded_instance_config] + : [] + ) + iterator = config + content { + enable_secure_boot = config.value.enable_secure_boot + enable_integrity_monitoring = config.value.enable_integrity_monitoring + } + } + + workload_metadata_config { + node_metadata = var.node_config_workload_metadata_config + } + + } + + dynamic autoscaling { + for_each = var.autoscaling_config != null ? [var.autoscaling_config] : [] + iterator = config + content { + min_node_count = config.min_node_count + max_node_count = config.max_node_count + } + } + + dynamic management { + for_each = var.management_config != null ? [var.management_config] : [] + iterator = config + content { + auto_repair = config.auto_repair + auto_upgrade = config.auto_upgrade + } + } + + dynamic upgrade_settings { + for_each = var.upgrade_config != null ? [var.upgrade_config] : [] + iterator = config + content { + max_surge = config.max_surge + max_unavailable = config.max_unavailable + } + } +} diff --git a/modules/gke-nodepool/outputs.tf b/modules/gke-nodepool/outputs.tf new file mode 100644 index 000000000..8645a91da --- /dev/null +++ b/modules/gke-nodepool/outputs.tf @@ -0,0 +1,20 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "name" { + description = "Nodepool name." + value = google_container_node_pool.nodepool.name +} diff --git a/modules/gke-nodepool/variables.tf b/modules/gke-nodepool/variables.tf new file mode 100644 index 000000000..049cfb3ed --- /dev/null +++ b/modules/gke-nodepool/variables.tf @@ -0,0 +1,198 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "autoscaling_config" { + description = "Optional autoscaling configuration." + type = object({ + min_node_count = number + max_node_count = number + }) + default = null +} + +variable "cluster_name" { + description = "Cluster name." + type = string +} + +variable "gke_version" { + description = "Kubernetes nodes version. Ignored if auto_upgrade is set in management_config." + type = string + default = null +} + +variable "initial_node_count" { + description = "Initial number of nodes for the pool." + type = number + default = 1 +} + +variable "location" { + description = "Cluster location." + type = string +} + +variable "management_config" { + description = "Optional node management configuration." + type = object({ + auto_repair = bool + auto_upgrade = bool + }) + default = null +} + +variable "max_pods_per_node" { + description = "Maximum number of pods per node." + type = number + default = null +} + +variable "name" { + description = "Optional nodepool name." + type = string + default = null +} + +variable "node_config_disk_size" { + description = "Node disk size, defaults to 100GB." + type = number + default = 100 +} + +variable "node_config_disk_type" { + description = "Node disk type, defaults to pd-standard." + type = string + default = "pd-standard" +} + +variable "node_config_guest_accelerator" { + description = "Map of type and count of attached accelerator cards." + type = map(number) + default = {} +} + +variable "node_config_image_type" { + description = "Nodes image type." + type = string + default = null +} + +variable "node_config_labels" { + description = "Kubernetes labels attached to nodes." + type = map(string) + default = {} +} + +variable "node_config_local_ssd_count" { + description = "Number of local SSDs attached to nodes." + type = number + default = 0 +} + +variable "node_config_machine_type" { + description = "Nodes machine type." + type = string + default = "n1-standard-1" +} + +variable "node_config_metadata" { + description = "Metadata key/value pairs assigned to nodes. Set disable-legacy-endpoints to true when using this variable." + type = map(string) + default = null +} + +variable "node_config_min_cpu_platform" { + description = "Minimum CPU platform for nodes." + type = string + default = null +} + +variable "node_config_oauth_scopes" { + description = "Set of Google API scopes for the nodes service account. Include logging-write, monitoring, and storage-ro when using this variable." + type = list(string) + default = ["logging-write", "monitoring", "monitoring-write", "storage-ro"] +} + +variable "node_config_preemptible" { + description = "Use preemptible VMs for nodes." + type = bool + default = null +} + +variable "node_config_sandbox_config" { + description = "GKE Sandbox configuration. Needs image_type set to COS_CONTAINERD and node_version set to 1.12.7-gke.17 when using this variable." + type = string + default = null +} + +variable "node_config_service_account" { + description = "Service account used for nodes." + type = string + default = null +} + +variable "node_config_shielded_instance_config" { + description = "Shielded instance options." + type = object({ + enable_secure_boot = bool + enable_integrity_monitoring = bool + }) + default = null +} + +variable "node_config_tags" { + description = "Network tags applied to nodes." + type = list(string) + default = null +} + +# variable "node_config_taint" { +# description = "Kubernetes taints applied to nodes." +# type = string +# default = null +# } + +variable "node_config_workload_metadata_config" { + description = "Metadata configuration to expose to workloads on the node pool." + type = string + default = "SECURE" +} + +variable "node_count" { + description = "Number of nodes per instance group, can be updated after creation. Ignored when autoscaling is set." + type = number + default = null +} + +variable "node_locations" { + description = "Optional list of zones in which nodes should be located. Uses cluster locations if unset." + type = list(string) + default = null +} + +variable "project_id" { + description = "Cluster project id." + type = string +} + +variable "upgrade_config" { + description = "Optional node upgrade configuration." + type = object({ + max_surge = number + max_unavailable = number + }) + default = null +} diff --git a/modules/gke-nodepool/versions.tf b/modules/gke-nodepool/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/gke-nodepool/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/iam-service-accounts/README.md b/modules/iam-service-accounts/README.md new file mode 100644 index 000000000..00be1075d --- /dev/null +++ b/modules/iam-service-accounts/README.md @@ -0,0 +1,59 @@ +# Google Service Accounts Module + +This module allows simplified creation and management of one or more service accounts and their IAM bindings. Keys can optionally be generated and will be stored in Terraform state. To use them create a sensitive output in your root modules referencing the `keys` or `key` outputs, then extract the private key from the JSON formatted outputs. + +## Example + +```hcl +module "myproject-default-service-accounts" { + source = "./modules/iam-service-accounts" + project_id = "myproject" + names = ["vm-default", "gke-node-default"] + generate_keys = true + # authoritative roles granted *on* the service accounts to other identities + iam_roles = ["roles/iam.serviceAccountUser"] + iam_members = { + "roles/iam.serviceAccountUser" => ["user:foo@example.com"] + } + # non-authoritative roles granted *to* the service accounts on other resources + iam_project_roles = { + "myproject" = [ + "roles/logging.logWriter", + "roles/monitoring.metricWriter", + ] + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| project_id | Project id where service account will be created. | string | ✓ | | +| *generate_keys* | Generate keys for service accounts. | bool | | false | +| *iam_billing_roles* | Project roles granted to all service accounts, by billing account id. | map(list(string)) | | {} | +| *iam_folder_roles* | Project roles granted to all service accounts, by folder id. | map(list(string)) | | {} | +| *iam_members* | Map of member lists which are granted authoritative roles on the service accounts, keyed by role. | map(list(string)) | | {} | +| *iam_organization_roles* | Project roles granted to all service accounts, by organization id. | map(list(string)) | | {} | +| *iam_project_roles* | Project roles granted to all service accounts, by project id. | map(list(string)) | | {} | +| *iam_roles* | List of authoritative roles granted on the service accounts. | list(string) | | [] | +| *iam_storage_roles* | Storage roles granted to all service accounts, by bucket name. | map(list(string)) | | {} | +| *names* | Names of the service accounts to create. | list(string) | | [] | +| *prefix* | Prefix applied to service account names. | string | | | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| email | Service account email (for single use). | | +| emails | Service account emails. | | +| emails_list | Service account emails. | | +| iam_email | IAM-format service account email (for single use). | | +| iam_emails | IAM-format service account emails. | | +| iam_emails_list | IAM-format service account emails. | | +| key | Service account key (for single use). | | +| keys | Map of service account keys. | ✓ | +| service_account | Service account resource (for single use). | | +| service_accounts | Service account resources. | | + diff --git a/modules/iam-service-accounts/main.tf b/modules/iam-service-accounts/main.tf new file mode 100644 index 000000000..21524fa34 --- /dev/null +++ b/modules/iam-service-accounts/main.tf @@ -0,0 +1,155 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + iam_pairs = { + for pair in setproduct(var.names, var.iam_roles) : + "${pair.0}-${pair.1}" => { name = pair.0, role = pair.1 } + } + iam_billing_pairs = flatten([ + for entity, roles in var.iam_billing_roles : [ + for role in roles : [ + for name in var.names : { entity = entity, role = role, name = name } + ] + ] + ]) + iam_folder_pairs = flatten([ + for entity, roles in var.iam_folder_roles : [ + for role in roles : [ + for name in var.names : { entity = entity, role = role, name = name } + ] + ] + ]) + iam_organization_pairs = flatten([ + for entity, roles in var.iam_organization_roles : [ + for role in roles : [ + for name in var.names : { entity = entity, role = role, name = name } + ] + ] + ]) + iam_project_pairs = flatten([ + for entity, roles in var.iam_project_roles : [ + for role in roles : [ + for name in var.names : { entity = entity, role = role, name = name } + ] + ] + ]) + iam_storage_pairs = flatten([ + for entity, roles in var.iam_storage_roles : [ + for role in roles : [ + for name in var.names : { entity = entity, role = role, name = name } + ] + ] + ]) + keys = ( + var.generate_keys + ? { + for name in var.names : + name => lookup(google_service_account_key.keys, name, null) + } + : {} + ) + prefix = ( + var.prefix != "" + ? "${var.prefix}-" + : "" + ) + resource = ( + length(var.names) > 0 + ? lookup(local.resources, var.names[0], null) + : null + ) + resource_iam_emails = { + for name, resource in local.resources : + name => "serviceAccount:${resource.email}" + } + resources = { + for name in var.names : + name => lookup(google_service_account.service_accounts, name, null) + } +} + +resource "google_service_account" "service_accounts" { + for_each = toset(var.names) + project = var.project_id + account_id = "${local.prefix}${lower(each.value)}" + display_name = "Terraform-managed." +} + +resource "google_service_account_key" "keys" { + for_each = var.generate_keys ? toset(var.names) : toset([]) + service_account_id = google_service_account.service_accounts[each.value].email +} + +resource "google_service_account_iam_binding" "sa-roles" { + for_each = local.iam_pairs + service_account_id = google_service_account.service_accounts[each.value.name].name + role = each.value.role + members = lookup(var.iam_members, each.value.role, []) +} + +resource "google_billing_account_iam_member" "roles" { + for_each = { + for pair in local.iam_billing_pairs : + "${pair.name}-${pair.entity}-${pair.role}" => pair + } + billing_account_id = each.value.entity + role = each.value.role + member = local.resource_iam_emails[each.value.name] +} + +resource "google_folder_iam_member" "roles" { + for_each = { + for pair in local.iam_folder_pairs : + "${pair.name}-${pair.entity}-${pair.role}" => pair + } + folder = each.value.entity + role = each.value.role + member = local.resource_iam_emails[each.value.name] +} + +resource "google_organization_iam_member" "roles" { + for_each = { + for pair in local.iam_organization_pairs : + "${pair.name}-${pair.entity}-${pair.role}" => pair + } + org_id = each.value.entity + role = each.value.role + member = local.resource_iam_emails[each.value.name] +} + +resource "google_project_iam_member" "project-roles" { + for_each = { + for pair in local.iam_project_pairs : + "${pair.name}-${pair.entity}-${pair.role}" => pair + } + project = each.value.entity + role = each.value.role + member = local.resource_iam_emails[each.value.name] +} + +resource "google_storage_bucket_iam_member" "bucket-roles" { + for_each = { + for pair in local.iam_storage_pairs : + "${pair.name}-${pair.entity}-${pair.role}" => pair + } + bucket = each.value.entity + role = each.value.role + member = local.resource_iam_emails[each.value.name] +} + +# TODO(ludoo): link from README +# ref: https://cloud.google.com/vpc/docs/shared-vpc diff --git a/modules/iam-service-accounts/outputs.tf b/modules/iam-service-accounts/outputs.tf new file mode 100644 index 000000000..9901675ef --- /dev/null +++ b/modules/iam-service-accounts/outputs.tf @@ -0,0 +1,66 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "service_account" { + description = "Service account resource (for single use)." + value = local.resource +} + +output "service_accounts" { + description = "Service account resources." + value = local.resources +} + +output "email" { + description = "Service account email (for single use)." + value = local.resource == null ? null : local.resource.email +} + +output "iam_email" { + description = "IAM-format service account email (for single use)." + value = local.resource == null ? null : "serviceAccount:${local.resource.email}" +} + +output "key" { + description = "Service account key (for single use)." + value = lookup(local.keys, var.names[0], null) +} + +output "emails" { + description = "Service account emails." + value = { for name, resource in local.resources : name => resource.email } +} + +output "iam_emails" { + description = "IAM-format service account emails." + value = local.resource_iam_emails +} + +output "emails_list" { + description = "Service account emails." + value = [for name, resource in local.resources : resource.email] +} + +output "iam_emails_list" { + description = "IAM-format service account emails." + value = [for name, resource in local.resources : "serviceAccount:${resource.email}"] +} + +output "keys" { + description = "Map of service account keys." + sensitive = true + value = local.keys +} diff --git a/modules/iam-service-accounts/variables.tf b/modules/iam-service-accounts/variables.tf new file mode 100644 index 000000000..60e0ed5fb --- /dev/null +++ b/modules/iam-service-accounts/variables.tf @@ -0,0 +1,80 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "generate_keys" { + description = "Generate keys for service accounts." + type = bool + default = false +} + +variable "names" { + description = "Names of the service accounts to create." + type = list(string) + default = [] +} + +variable "prefix" { + description = "Prefix applied to service account names." + type = string + default = "" +} + +variable "project_id" { + description = "Project id where service account will be created." + type = string +} + +variable "iam_members" { + description = "Map of member lists which are granted authoritative roles on the service accounts, keyed by role." + type = map(list(string)) + default = {} +} + +variable "iam_roles" { + description = "List of authoritative roles granted on the service accounts." + type = list(string) + default = [] +} + +variable "iam_billing_roles" { + description = "Project roles granted to all service accounts, by billing account id." + type = map(list(string)) + default = {} +} + +variable "iam_folder_roles" { + description = "Project roles granted to all service accounts, by folder id." + type = map(list(string)) + default = {} +} + +variable "iam_organization_roles" { + description = "Project roles granted to all service accounts, by organization id." + type = map(list(string)) + default = {} +} + +variable "iam_project_roles" { + description = "Project roles granted to all service accounts, by project id." + type = map(list(string)) + default = {} +} + +variable "iam_storage_roles" { + description = "Storage roles granted to all service accounts, by bucket name." + type = map(list(string)) + default = {} +} diff --git a/modules/iam-service-accounts/versions.tf b/modules/iam-service-accounts/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/iam-service-accounts/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/kms/README.md b/modules/kms/README.md new file mode 100644 index 000000000..55a0e944e --- /dev/null +++ b/modules/kms/README.md @@ -0,0 +1,83 @@ +# Google KMS Module + +Simple Cloud KMS module that allows managing a keyring, zero or more keys in the keyring, and IAM role bindings on individual keys. + +The `protected` flag in the `key_attributes` variable sets the `prevent_destroy` lifecycle argument on an a per-key basis. + +## Examples + +### Minimal example + +```hcl +module "kms" { + source = "../modules/kms" + project_id = "my-project" + keyring = "test" + location = "europe" + keys = ["key-a", "key-b"] +} +``` + +### Granting access to keys via IAM + +```hcl +module "kms" { + source = "../modules/kms" + project_id = "my-project" + keyring = "test" + location = "europe" + keys = ["key-a", "key-b"] + iam_roles = { + key-a = ["roles/cloudkms.cryptoKeyDecrypter"] + } + iam_members = { + key-a = { + "roles/cloudkms.cryptoKeyDecrypter" = ["user:me@example.org"] + } + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| keyring | Keyring name. | string | ✓ | | +| location | Location for the keyring. | string | ✓ | | +| project_id | Project id where the keyring will be created. | string | ✓ | | +| *iam_members* | IAM members keyed by key name and role. | map(map(list(string))) | | {} | +| *iam_roles* | IAM roles keyed by key name. | map(list(string)) | | {} | +| *key_attributes* | Optional key attributes per key. | map(object({...})) | | {} | +| *key_defaults* | Key attribute defaults. | object({...}) | | ... | +| *keys* | Key names. | list(string) | | [] | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| key_self_links | Key self links. | | +| keyring | Keyring resource. | | +| keys | Key resources. | | +| location | Keyring self link. | | +| name | Keyring self link. | | +| self_link | Keyring self link. | | + + +## Requirements + +These sections describe requirements for using this module. + +### IAM + +The following roles must be used to provision the resources of this module: + +- Cloud KMS Admin: `roles/cloudkms.admin` or +- Owner: `roles/owner` + +### APIs + +A project with the following APIs enabled must be used to host the +resources of this module: + +- Google Cloud Key Management Service: `cloudkms.googleapis.com` diff --git a/modules/kms/main.tf b/modules/kms/main.tf new file mode 100644 index 000000000..18c14eff9 --- /dev/null +++ b/modules/kms/main.tf @@ -0,0 +1,73 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + # distinct is needed to make the expanding function argument work + iam_pairs = flatten([ + for name, roles in var.iam_roles : + [for role in roles : { name = name, role = role }] + ]) + iam_keypairs = { + for pair in local.iam_pairs : + "${pair.name}-${pair.role}" => pair + } + key_attributes = { + for name in var.keys : + name => lookup(var.key_attributes, name, var.key_defaults) + } + keys = merge( + { for name, resource in google_kms_crypto_key.keys : name => resource }, + { for name, resource in google_kms_crypto_key.keys-ephemeral : name => resource } + ) +} + +resource "google_kms_key_ring" "key_ring" { + name = var.keyring + project = var.project_id + location = var.location +} + +resource "google_kms_crypto_key" "keys" { + for_each = { + for name, attrs in local.key_attributes : + name => attrs if attrs.protected + } + name = each.key + key_ring = google_kms_key_ring.key_ring.self_link + rotation_period = each.value.rotation_period + lifecycle { + prevent_destroy = true + } +} + +resource "google_kms_crypto_key" "keys-ephemeral" { + for_each = { + for name, attrs in local.key_attributes : + name => attrs if ! attrs.protected + } + name = each.key + key_ring = google_kms_key_ring.key_ring.self_link + rotation_period = each.value.rotation_period +} + +resource "google_kms_crypto_key_iam_binding" "bindings" { + for_each = local.iam_keypairs + role = each.value.role + crypto_key_id = local.keys[each.value.name].self_link + members = lookup( + lookup(var.iam_members, each.value.name, {}), each.value.role, [] + ) +} diff --git a/modules/kms/outputs.tf b/modules/kms/outputs.tf new file mode 100644 index 000000000..de30ea172 --- /dev/null +++ b/modules/kms/outputs.tf @@ -0,0 +1,45 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "keyring" { + description = "Keyring resource." + value = google_kms_key_ring.key_ring +} + +output "location" { + description = "Keyring self link." + value = google_kms_key_ring.key_ring.location +} + +output "name" { + description = "Keyring self link." + value = google_kms_key_ring.key_ring.name +} + +output "self_link" { + description = "Keyring self link." + value = google_kms_key_ring.key_ring.self_link +} + +output "keys" { + description = "Key resources." + value = local.keys +} + +output "key_self_links" { + description = "Key self links." + value = { for name, resource in local.keys : name => resource.self_link } +} diff --git a/modules/kms/variables.tf b/modules/kms/variables.tf new file mode 100644 index 000000000..e976b72b2 --- /dev/null +++ b/modules/kms/variables.tf @@ -0,0 +1,70 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "iam_members" { + description = "IAM members keyed by key name and role." + type = map(map(list(string))) + default = {} +} + +variable "iam_roles" { + description = "IAM roles keyed by key name." + type = map(list(string)) + default = {} +} + +variable "keyring" { + description = "Keyring name." + type = string +} + +variable "key_attributes" { + description = "Optional key attributes per key." + type = map(object({ + protected = bool + rotation_period = string + })) + default = {} +} + +variable "key_defaults" { + description = "Key attribute defaults." + type = object({ + protected = bool + rotation_period = string + }) + default = { + protected = true + rotation_period = "100000s" + } +} + +variable "keys" { + description = "Key names." + type = list(string) + default = [] +} + +# cf https://cloud.google.com/kms/docs/locations +variable "location" { + description = "Location for the keyring." + type = string +} + +variable "project_id" { + description = "Project id where the keyring will be created." + type = string +} diff --git a/modules/kms/versions.tf b/modules/kms/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/kms/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/logging-sinks/README.md b/modules/logging-sinks/README.md new file mode 100644 index 000000000..c136e97d5 --- /dev/null +++ b/modules/logging-sinks/README.md @@ -0,0 +1,31 @@ +# Terraform Logging Sinks Module + +This module allows easy creation of one or more logging sinks. + +## Example + +```hcl +module "sinks" { +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| destinations | Map of destinations by sink name. | map(string) | ✓ | | +| parent | Resource where the sink will be created, eg 'organizations/nnnnnnnn'. | string | ✓ | | +| sinks | Map of sink name / sink filter. | map(string) | ✓ | | +| *default_options* | Default options used for sinks where no specific options are set. | object({...}) | | ... | +| *sink_options* | Optional map of sink name / sink options. If no options are specified for a sink defaults will be used. | map(object({...})) | | {} | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| names | Log sink names. | | +| sinks | Log sink resources. | | +| writer_identities | Log sink writer identities. | | + + diff --git a/modules/logging-sinks/main.tf b/modules/logging-sinks/main.tf new file mode 100644 index 000000000..cc9d83176 --- /dev/null +++ b/modules/logging-sinks/main.tf @@ -0,0 +1,97 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + bigquery_destinations = { + for name, destination in var.destinations : + name => substr(destination, 0, 8) == "bigquery" + } + resource_type = element(split("/", var.parent), 0) + resource_id = element(split("/", var.parent), 1) + sink_options = { + for name, _ in var.sinks : + name => lookup(var.sink_options, name, var.default_options) + } + sink_resources = concat( + [for _, sink in google_logging_organization_sink.sinks : sink], + [for _, sink in google_logging_billing_account_sink.sinks : sink], + [for _, sink in google_logging_folder_sink.sinks : sink], + [for _, sink in google_logging_project_sink.sinks : sink], + ) +} + +resource "google_logging_organization_sink" "sinks" { + for_each = local.resource_type == "organizations" ? var.sinks : {} + name = each.key + org_id = local.resource_id + filter = each.value + destination = var.destinations[each.key] + include_children = local.sink_options[each.key].include_children + dynamic bigquery_options { + for_each = local.bigquery_destinations[each.key] ? ["1"] : [] + iterator = config + content { + use_partitioned_tables = local.sink_options[each.key].bigquery_partitioned_tables + } + } +} + +resource "google_logging_billing_account_sink" "sinks" { + for_each = local.resource_type == "billing_accounts" ? var.sinks : {} + name = each.key + billing_account = local.resource_id + filter = each.value + destination = var.destinations[each.key] + dynamic bigquery_options { + for_each = local.bigquery_destinations[each.key] ? ["1"] : [] + iterator = config + content { + use_partitioned_tables = local.sink_options[each.key].bigquery_partitioned_tables + } + } +} + +resource "google_logging_folder_sink" "sinks" { + for_each = local.resource_type == "folders" ? var.sinks : {} + name = each.key + folder = var.parent + filter = each.value + destination = var.destinations[each.key] + include_children = local.sink_options[each.key].include_children + dynamic bigquery_options { + for_each = local.bigquery_destinations[each.key] ? ["1"] : [] + iterator = config + content { + use_partitioned_tables = local.sink_options[each.key].bigquery_partitioned_tables + } + } +} + +resource "google_logging_project_sink" "sinks" { + for_each = local.resource_type == "projects" ? var.sinks : {} + name = each.key + project = local.resource_id + filter = each.value + destination = var.destinations[each.key] + unique_writer_identity = local.sink_options[each.key].unique_writer_identity + dynamic bigquery_options { + for_each = local.bigquery_destinations[each.key] ? ["1"] : [] + iterator = config + content { + use_partitioned_tables = local.sink_options[each.key].bigquery_partitioned_tables + } + } +} diff --git a/modules/logging-sinks/outputs.tf b/modules/logging-sinks/outputs.tf new file mode 100644 index 000000000..dd8fc8dba --- /dev/null +++ b/modules/logging-sinks/outputs.tf @@ -0,0 +1,30 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "sinks" { + description = "Log sink resources." + value = local.sink_resources +} + +output "names" { + description = "Log sink names." + value = [for sink in local.sink_resources : sink.name] +} + +output "writer_identities" { + description = "Log sink writer identities." + value = [for sink in local.sink_resources : sink.writer_identity] +} diff --git a/modules/logging-sinks/variables.tf b/modules/logging-sinks/variables.tf new file mode 100644 index 000000000..24294e7d1 --- /dev/null +++ b/modules/logging-sinks/variables.tf @@ -0,0 +1,54 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "default_options" { + description = "Default options used for sinks where no specific options are set." + type = object({ + bigquery_partitioned_tables = bool + include_children = bool + unique_writer_identity = bool + }) + default = { + bigquery_partitioned_tables = true + include_children = true + unique_writer_identity = false + } +} + +variable "destinations" { + description = "Map of destinations by sink name." + type = map(string) +} + +variable "parent" { + description = "Resource where the sink will be created, eg 'organizations/nnnnnnnn'." + type = string +} + +variable "sink_options" { + description = "Optional map of sink name / sink options. If no options are specified for a sink defaults will be used." + type = map(object({ + bigquery_partitioned_tables = bool + include_children = bool + unique_writer_identity = bool + })) + default = {} +} + +variable "sinks" { + description = "Map of sink name / sink filter." + type = map(string) +} diff --git a/modules/logging-sinks/versions.tf b/modules/logging-sinks/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/logging-sinks/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/net-address/README.md b/modules/net-address/README.md new file mode 100644 index 000000000..9c1169b5b --- /dev/null +++ b/modules/net-address/README.md @@ -0,0 +1,35 @@ +# Net Address Reservation Module + +## Example + +```hcl +module "addresses" { + source = "./modules/net-address" + project_id = local.projects.host + external_addresses = { + nat-1 = module.vpc.subnet_regions["default"], + vpn-remote = module.vpc.subnet_regions["default"], + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| project_id | Project where the addresses will be created. | string | ✓ | | +| *external_addresses* | Map of external address regions, keyed by name. | map(string) | | {} | +| *global_addresses* | List of global addresses to create. | list(string) | | [] | +| *internal_address_addresses* | Optional explicit addresses for internal addresses, keyed by name. | map(string) | | {} | +| *internal_address_tiers* | Optional network tiers for internal addresses, keyed by name. | map(string) | | {} | +| *internal_addresses* | Map of internal addresses to create, keyed by name. | map(object({...})) | | {} | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| external_addresses | None | | +| global_addresses | None | | +| internal_addresses | None | | + \ No newline at end of file diff --git a/modules/net-address/main.tf b/modules/net-address/main.tf new file mode 100644 index 000000000..b752f2aa9 --- /dev/null +++ b/modules/net-address/main.tf @@ -0,0 +1,44 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_global_address" "global" { + for_each = toset(var.global_addresses) + project = var.project_id + name = each.value +} + +resource "google_compute_address" "external" { + for_each = var.external_addresses + project = var.project_id + name = each.key + description = "Terraform managed." + address_type = "EXTERNAL" + region = each.value + # labels = lookup(var.external_address_labels, each.key, {}) +} + +resource "google_compute_address" "internal" { + for_each = var.internal_addresses + project = var.project_id + name = each.key + description = "Terraform managed." + address_type = "INTERNAL" + region = each.value.region + subnetwork = each.value.subnetwork + address = lookup(var.internal_address_addresses, each.key, null) + network_tier = lookup(var.internal_address_tiers, each.key, null) + # labels = lookup(var.internal_address_labels, each.key, {}) +} diff --git a/modules/net-address/outputs.tf b/modules/net-address/outputs.tf new file mode 100644 index 000000000..7d26158a6 --- /dev/null +++ b/modules/net-address/outputs.tf @@ -0,0 +1,48 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "external_addresses" { + value = { + for address in google_compute_address.external : + address.name => { + address = address.address + self_link = address.self_link + users = address.users + } + } +} + +output "global_addresses" { + value = { + for address in google_compute_global_address.global : + address.name => { + address = address.address + self_link = address.self_link + status = address.status + } + } +} + +output "internal_addresses" { + value = { + for address in google_compute_address.internal : + address.name => { + address = address.address + self_link = address.self_link + users = address.users + } + } +} diff --git a/modules/net-address/variables.tf b/modules/net-address/variables.tf new file mode 100644 index 000000000..02b85f68b --- /dev/null +++ b/modules/net-address/variables.tf @@ -0,0 +1,65 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "external_addresses" { + description = "Map of external address regions, keyed by name." + type = map(string) + default = {} +} + +# variable "external_address_labels" { +# description = "Optional labels for external addresses, keyed by address name." +# type = map(map(string)) +# default = {} +# } + +variable "global_addresses" { + description = "List of global addresses to create." + type = list(string) + default = [] +} + +variable "internal_addresses" { + description = "Map of internal addresses to create, keyed by name." + type = map(object({ + region = string + subnetwork = string + })) + default = {} +} + +variable "internal_address_addresses" { + description = "Optional explicit addresses for internal addresses, keyed by name." + type = map(string) + default = {} +} + +variable "internal_address_tiers" { + description = "Optional network tiers for internal addresses, keyed by name." + type = map(string) + default = {} +} + +# variable "internal_address_labels" { +# description = "Optional labels for internal addresses, keyed by address name." +# type = map(map(string)) +# default = {} +# } + +variable "project_id" { + description = "Project where the addresses will be created." + type = string +} diff --git a/modules/net-address/versions.tf b/modules/net-address/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/net-address/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/net-cloudnat/README.md b/modules/net-cloudnat/README.md new file mode 100644 index 000000000..b8bb17ce2 --- /dev/null +++ b/modules/net-cloudnat/README.md @@ -0,0 +1,44 @@ +# Cloud NAT Module + +Simple Cloud NAT management, with optional router creation. + +## Example + +```hcl +module "nat" { + source = "../modules/net-cloudnat" + project_id = "my-project" + region = "europe-west1" + name = "default" + router_network = "my-vpc" +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| name | Name of the Cloud NAT resource. | string | ✓ | | +| project_id | Project where resources will be created. | string | ✓ | | +| region | Region where resources will be created. | string | ✓ | | +| *addresses* | Optional list of external address self links. | list(string) | | [] | +| *config_min_ports_per_vm* | Minimum number of ports allocated to a VM from this NAT config. | number | | 64 | +| *config_source_subnets* | Subnetwork configuration (ALL_SUBNETWORKS_ALL_IP_RANGES, ALL_SUBNETWORKS_ALL_PRIMARY_IP_RANGES, LIST_OF_SUBNETWORKS). | string | | ALL_SUBNETWORKS_ALL_IP_RANGES | +| *config_timeouts* | Timeout configurations. | object({...}) | | ... | +| *router_asn* | Router ASN used for auto-created router. | number | | 64514 | +| *router_create* | Create router. | bool | | true | +| *router_name* | Router name, leave blank if router will be created to use auto generated name. | string | | | +| *router_network* | Name of the VPC used for auto-created router. | string | | | +| *subnetworks* | Subnetworks to NAT, only used when config_source_subnets equals LIST_OF_SUBNETWORKS. | list(object({...})) | | [] | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| name | Name of the Cloud NAT. | | +| nat_ip_allocate_option | NAT IP allocation mode. | | +| region | Cloud NAT region. | | +| router | Cloud NAT router resources (if auto created). | | +| router_name | Cloud NAT router name. | | + diff --git a/modules/net-cloudnat/main.tf b/modules/net-cloudnat/main.tf new file mode 100644 index 000000000..fa652ff03 --- /dev/null +++ b/modules/net-cloudnat/main.tf @@ -0,0 +1,57 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + router_name = ( + var.router_create ? google_compute_router.router[0].name : var.router_name + ) +} + +resource "google_compute_router" "router" { + count = var.router_create ? 1 : 0 + name = var.router_name == "" ? "${var.name}-nat" : var.router_name + project = var.project_id + region = var.region + network = var.router_network + bgp { + asn = var.router_asn + } +} + +resource "google_compute_router_nat" "nat" { + project = var.project_id + region = var.region + name = var.name + router = local.router_name + nat_ips = var.addresses + nat_ip_allocate_option = length(var.addresses) > 0 ? "MANUAL_ONLY" : "AUTO_ONLY" + source_subnetwork_ip_ranges_to_nat = var.config_source_subnets + min_ports_per_vm = var.config_min_ports_per_vm + icmp_idle_timeout_sec = var.config_timeouts.icmp + udp_idle_timeout_sec = var.config_timeouts.udp + tcp_established_idle_timeout_sec = var.config_timeouts.tcp_established + tcp_transitory_idle_timeout_sec = var.config_timeouts.tcp_transitory + + dynamic "subnetwork" { + for_each = var.subnetworks + content { + name = subnetwork.value.self_link + source_ip_ranges_to_nat = subnetwork.value.config_source_ranges + secondary_ip_range_names = subnetwork.value.secondary_ranges + } + } +} + diff --git a/modules/net-cloudnat/outputs.tf b/modules/net-cloudnat/outputs.tf new file mode 100644 index 000000000..8dad02349 --- /dev/null +++ b/modules/net-cloudnat/outputs.tf @@ -0,0 +1,40 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "name" { + description = "Name of the Cloud NAT." + value = google_compute_router_nat.nat.name +} + +output "nat_ip_allocate_option" { + description = "NAT IP allocation mode." + value = google_compute_router_nat.nat.nat_ip_allocate_option +} + +output "region" { + description = "Cloud NAT region." + value = google_compute_router_nat.nat.region +} + +output "router" { + description = "Cloud NAT router resources (if auto created)." + value = var.router_create ? google_compute_router.router[0] : null +} + +output "router_name" { + description = "Cloud NAT router name." + value = local.router_name +} diff --git a/modules/net-cloudnat/variables.tf b/modules/net-cloudnat/variables.tf new file mode 100644 index 000000000..5b987cf56 --- /dev/null +++ b/modules/net-cloudnat/variables.tf @@ -0,0 +1,98 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "addresses" { + description = "Optional list of external address self links." + type = list(string) + default = [] +} + +variable "config_min_ports_per_vm" { + description = "Minimum number of ports allocated to a VM from this NAT config." + type = number + default = 64 +} + +variable "config_source_subnets" { + description = "Subnetwork configuration (ALL_SUBNETWORKS_ALL_IP_RANGES, ALL_SUBNETWORKS_ALL_PRIMARY_IP_RANGES, LIST_OF_SUBNETWORKS)." + type = string + default = "ALL_SUBNETWORKS_ALL_IP_RANGES" +} + +variable "config_timeouts" { + description = "Timeout configurations." + type = object({ + icmp = number + tcp_established = number + tcp_transitory = number + udp = number + }) + default = { + icmp = 30 + tcp_established = 1200 + tcp_transitory = 30 + udp = 30 + } +} + +variable "name" { + description = "Name of the Cloud NAT resource." + type = string +} + +variable "project_id" { + description = "Project where resources will be created." + type = string +} + +variable "region" { + description = "Region where resources will be created." + type = string +} + +variable "router_asn" { + description = "Router ASN used for auto-created router." + type = number + default = 64514 +} + +variable "router_create" { + description = "Create router." + type = bool + default = true +} + +variable "router_name" { + description = "Router name, leave blank if router will be created to use auto generated name." + type = string + default = "" +} + +variable "router_network" { + description = "Name of the VPC used for auto-created router." + type = string + default = "" +} + +variable "subnetworks" { + description = "Subnetworks to NAT, only used when config_source_subnets equals LIST_OF_SUBNETWORKS." + type = list(object({ + self_link = string, + config_source_ranges = list(string) + secondary_ranges = list(string) + })) + default = [] +} diff --git a/modules/net-cloudnat/versions.tf b/modules/net-cloudnat/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/net-cloudnat/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/net-vpc-firewall/README.md b/modules/net-vpc-firewall/README.md new file mode 100644 index 000000000..23b24eded --- /dev/null +++ b/modules/net-vpc-firewall/README.md @@ -0,0 +1,77 @@ +# Google Cloud VPC Firewall + +This module allows creation and management of different types of firewall rules for a single VPC network: + +- blanket ingress rules based on IP ranges that allow all traffic via the `admin_ranges` variable +- simplified tag-based ingress rules for the HTTP, HTTPS and SSH protocols via the `xxx_source_ranges` variables; HTTP and HTTPS tags match those set by the console via the "Allow HTTP(S) traffic" instance flags +- custom rules via the `custom_rules` variables + +The simplified tag-based rules are enabled by default, set to the ranges of the GCP health checkers for HTTP/HTTPS, and the IAP forwarders for SSH. To disable them set the corresponding variables to empty lists. + +## Examples + +### Minimal open firewall + +This is often useful for prototyping or testing infrastructure, allowing open ingress from the private range, enabling SSH to private addresses from IAP, and HTTP/HTTPS from the health checkers. + +```hcl +module "firewall" { + source = "./modules/net-vpc-firewall" + project_id = "my-project" + network = "my-network" + admin_ranges_enabled = true + admin_ranges = ["10.0.0.0/8"] +} +``` + +### Custom rules + +This is an example of how to define custom rules, with a sample rule allowing open ingress for the NTP protocol to instances with the `ntp-svc` tag. + +```hcl +module "firewall" { + source = "../modules/net-vpc-firewall" + project_id = "my-project" + network = "my-network" + admin_ranges_enabled = true + admin_ranges = ["10.0.0.0/8"] + custom_rules = { + ntp-svc = { + description = "NTP service." + direction = "INGRESS" + action = "allow" + sources = [] + ranges = ["0.0.0.0/0"] + targets = ["ntp-svc"] + use_service_accounts = false + rules = [{ protocol = "udp", ports = [123] }] + extra_attributes = {} + } + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| network | Name of the network this set of firewall rules applies to. | string | ✓ | | +| project_id | Project id of the project that holds the network. | string | ✓ | | +| *admin_ranges* | IP CIDR ranges that have complete access to all subnets. | list(string) | | [] | +| *admin_ranges_enabled* | Enable admin ranges-based rules. | bool | | false | +| *custom_rules* | List of custom rule definitions (refer to variables file for syntax). | map(object({...})) | | {} | +| *http_source_ranges* | List of IP CIDR ranges for tag-based HTTP rule, defaults to the health checkers ranges. | list(string) | | ["35.191.0.0/16", "130.211.0.0/22"] | +| *https_source_ranges* | List of IP CIDR ranges for tag-based HTTPS rule, defaults to the health checkers ranges. | list(string) | | ["35.191.0.0/16", "130.211.0.0/22"] | +| *ssh_source_ranges* | List of IP CIDR ranges for tag-based SSH rule, defaults to the IAP forwarders range. | list(string) | | ["35.235.240.0/20"] | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| admin_ranges | Admin ranges data. | | +| custom_egress_allow_rules | Custom egress rules with allow blocks. | | +| custom_egress_deny_rules | Custom egress rules with allow blocks. | | +| custom_ingress_allow_rules | Custom ingress rules with allow blocks. | | +| custom_ingress_deny_rules | Custom ingress rules with deny blocks. | | + diff --git a/modules/net-vpc-firewall/main.tf b/modules/net-vpc-firewall/main.tf new file mode 100644 index 000000000..7252e3a8a --- /dev/null +++ b/modules/net-vpc-firewall/main.tf @@ -0,0 +1,145 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + rules-allow = { + for name, attrs in var.custom_rules : name => attrs if attrs.action == "allow" + } + rules-deny = { + for name, attrs in var.custom_rules : name => attrs if attrs.action == "deny" + } +} + +############################################################################### +# rules based on IP ranges +############################################################################### + +resource "google_compute_firewall" "allow-admins" { + count = var.admin_ranges_enabled == true ? 1 : 0 + name = "${var.network}-ingress-admins" + description = "Access from the admin subnet to all subnets" + network = var.network + project = var.project_id + source_ranges = var.admin_ranges + allow { protocol = "icmp" } + allow { protocol = "tcp" } + allow { protocol = "udp" } +} + +############################################################################### +# rules based on tags +############################################################################### + +resource "google_compute_firewall" "allow-tag-ssh" { + count = length(var.ssh_source_ranges) > 0 ? 1 : 0 + name = "${var.network}-ingress-tag-ssh" + description = "Allow SSH to machines with the 'ssh' tag" + network = var.network + project = var.project_id + source_ranges = var.ssh_source_ranges + target_tags = ["ssh"] + allow { + protocol = "tcp" + ports = ["22"] + } +} + +resource "google_compute_firewall" "allow-tag-http" { + count = length(var.http_source_ranges) > 0 ? 1 : 0 + name = "${var.network}-ingress-tag-http" + description = "Allow HTTP to machines with the 'http-server' tag" + network = var.network + project = var.project_id + source_ranges = var.http_source_ranges + target_tags = ["http-server"] + allow { + protocol = "tcp" + ports = ["80"] + } +} + +resource "google_compute_firewall" "allow-tag-https" { + count = length(var.https_source_ranges) > 0 ? 1 : 0 + name = "${var.network}-ingress-tag-https" + description = "Allow HTTPS to machines with the 'https' tag" + network = var.network + project = var.project_id + source_ranges = var.https_source_ranges + target_tags = ["https-server"] + allow { + protocol = "tcp" + ports = ["443"] + } +} + +################################################################################ +# dynamic rules # +################################################################################ + +resource "google_compute_firewall" "custom_allow" { + # provider = "google-beta" + for_each = local.rules-allow + name = each.key + description = each.value.description + direction = each.value.direction + network = var.network + project = var.project_id + source_ranges = each.value.direction == "INGRESS" ? each.value.ranges : null + destination_ranges = each.value.direction == "EGRESS" ? each.value.ranges : null + source_tags = each.value.use_service_accounts || each.value.direction == "EGRESS" ? null : each.value.sources + source_service_accounts = each.value.use_service_accounts && each.value.direction == "INGRESS" ? each.value.sources : null + target_tags = each.value.use_service_accounts ? null : each.value.targets + target_service_accounts = each.value.use_service_accounts ? each.value.targets : null + disabled = lookup(each.value.extra_attributes, "disabled", false) + priority = lookup(each.value.extra_attributes, "priority", 1000) + # enable_logging = lookup(each.value.extra_attributes, "enable_logging", false) + dynamic "allow" { + for_each = each.value.rules + iterator = rule + content { + protocol = rule.value.protocol + ports = rule.value.ports + } + } +} + +resource "google_compute_firewall" "custom_deny" { + # provider = "google-beta" + for_each = local.rules-deny + name = each.key + description = each.value.description + direction = each.value.direction + network = var.network + project = var.project_id + source_ranges = each.value.direction == "INGRESS" ? each.value.ranges : null + destination_ranges = each.value.direction == "EGRESS" ? each.value.ranges : null + source_tags = each.value.use_service_accounts || each.value.direction == "EGRESS" ? null : each.value.sources + source_service_accounts = each.value.use_service_accounts && each.value.direction == "INGRESS" ? each.value.sources : null + target_tags = each.value.use_service_accounts ? null : each.value.targets + target_service_accounts = each.value.use_service_accounts ? each.value.targets : null + disabled = lookup(each.value.extra_attributes, "disabled", false) + priority = lookup(each.value.extra_attributes, "priority", 1000) + # enable_logging = lookup(each.value.extra_attributes, "enable_logging", false) + + dynamic "deny" { + for_each = each.value.rules + iterator = rule + content { + protocol = rule.value.protocol + ports = rule.value.ports + } + } +} diff --git a/modules/net-vpc-firewall/outputs.tf b/modules/net-vpc-firewall/outputs.tf new file mode 100644 index 000000000..0819e9d11 --- /dev/null +++ b/modules/net-vpc-firewall/outputs.tf @@ -0,0 +1,56 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "admin_ranges" { + description = "Admin ranges data." + + value = { + enabled = var.admin_ranges_enabled + ranges = var.admin_ranges_enabled ? join(",", var.admin_ranges) : "" + } +} + +output "custom_ingress_allow_rules" { + description = "Custom ingress rules with allow blocks." + value = [ + for rule in google_compute_firewall.custom_allow : + rule.name if rule.direction == "INGRESS" + ] +} + +output "custom_ingress_deny_rules" { + description = "Custom ingress rules with deny blocks." + value = [ + for rule in google_compute_firewall.custom_deny : + rule.name if rule.direction == "INGRESS" + ] +} + +output "custom_egress_allow_rules" { + description = "Custom egress rules with allow blocks." + value = [ + for rule in google_compute_firewall.custom_allow : + rule.name if rule.direction == "EGRESS" + ] +} + +output "custom_egress_deny_rules" { + description = "Custom egress rules with allow blocks." + value = [ + for rule in google_compute_firewall.custom_deny : + rule.name if rule.direction == "EGRESS" + ] +} diff --git a/modules/net-vpc-firewall/variables.tf b/modules/net-vpc-firewall/variables.tf new file mode 100644 index 000000000..4c8f4fb5b --- /dev/null +++ b/modules/net-vpc-firewall/variables.tf @@ -0,0 +1,74 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "network" { + description = "Name of the network this set of firewall rules applies to." + type = string +} + +variable "project_id" { + description = "Project id of the project that holds the network." + type = string +} + +variable "admin_ranges_enabled" { + description = "Enable admin ranges-based rules." + type = bool + default = false +} + +variable "admin_ranges" { + description = "IP CIDR ranges that have complete access to all subnets." + type = list(string) + default = [] +} + +variable "ssh_source_ranges" { + description = "List of IP CIDR ranges for tag-based SSH rule, defaults to the IAP forwarders range." + type = list(string) + default = ["35.235.240.0/20"] +} + +variable "http_source_ranges" { + description = "List of IP CIDR ranges for tag-based HTTP rule, defaults to the health checkers ranges." + type = list(string) + default = ["35.191.0.0/16", "130.211.0.0/22"] +} + +variable "https_source_ranges" { + description = "List of IP CIDR ranges for tag-based HTTPS rule, defaults to the health checkers ranges." + type = list(string) + default = ["35.191.0.0/16", "130.211.0.0/22"] +} + +variable "custom_rules" { + description = "List of custom rule definitions (refer to variables file for syntax)." + type = map(object({ + description = string + direction = string + action = string # (allow|deny) + ranges = list(string) + sources = list(string) + targets = list(string) + use_service_accounts = bool + rules = list(object({ + protocol = string + ports = list(string) + })) + extra_attributes = map(string) + })) + default = {} +} diff --git a/modules/net-vpc-firewall/versions.tf b/modules/net-vpc-firewall/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/net-vpc-firewall/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/net-vpc-peering/README.md b/modules/net-vpc-peering/README.md new file mode 100644 index 000000000..f10994383 --- /dev/null +++ b/modules/net-vpc-peering/README.md @@ -0,0 +1,65 @@ +# Google Network Peering + +This module allows creation of a [VPC Network Peering](https://cloud.google.com/vpc/docs/vpc-peering) between two networks. + +The resources created/managed by this module are: + +- one network peering from `local network` to `peer network` +- one network peering from `peer network` to `local network` + +## Usage + +Basic usage of this module is as follows: + +```hcl +module "peering" { + source = "modules/net-vpc-peering" + + prefix = "name-prefix" + local_network = "" + peer_network = "" +} +``` + +If you need to create more than one peering for the same VPC Network `(A -> B, A -> C)` you have to use output from the first module as a dependency for the second one to keep order of peering creation (It is not currently possible to create more than one peering connection for a VPC Network at the same time). + +```hcl +module "peering-a-b" { + source = "modules/net-vpc-peering" + + prefix = "name-prefix" + local_network = "" + peer_network = "" +} + +module "peering-a-c" { + source = "modules/net-vpc-peering" + + prefix = "name-prefix" + local_network = "" + peer_network = "" + + module_depends_on = [module.peering-a-b.complete] +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| local_network | Resource link of the network to add a peering to. | string | ✓ | | +| peer_network | Resource link of the peer network. | string | ✓ | | +| *export_local_custom_routes* | Export custom routes to peer network from local network. | bool | | false | +| *export_peer_custom_routes* | Export custom routes to local network from peer network. | bool | | false | +| *module_depends_on* | List of modules or resources this module depends on. | list | | [] | +| *prefix* | Name prefix for the network peerings | string | | network-peering | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| complete | Output to be used as a module dependency. | | +| local_network_peering | Network peering resource. | | +| peer_network_peering | Peer network peering resource. | | + \ No newline at end of file diff --git a/modules/net-vpc-peering/main.tf b/modules/net-vpc-peering/main.tf new file mode 100644 index 000000000..3090505a1 --- /dev/null +++ b/modules/net-vpc-peering/main.tf @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.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. + */ + +locals { + local_network_name = element(reverse(split("/", var.local_network)), 0) + peer_network_name = element(reverse(split("/", var.peer_network)), 0) +} + +resource "google_compute_network_peering" "local_network_peering" { + name = "${var.prefix}-${local.local_network_name}-${local.peer_network_name}" + network = var.local_network + peer_network = var.peer_network + export_custom_routes = var.export_local_custom_routes + import_custom_routes = var.export_peer_custom_routes + + depends_on = [null_resource.module_depends_on] +} + +resource "google_compute_network_peering" "peer_network_peering" { + name = "${var.prefix}-${local.peer_network_name}-${local.local_network_name}" + network = var.peer_network + peer_network = var.local_network + export_custom_routes = var.export_peer_custom_routes + import_custom_routes = var.export_local_custom_routes + + depends_on = [null_resource.module_depends_on, google_compute_network_peering.local_network_peering] +} + +resource "null_resource" "module_depends_on" { + triggers = { + value = length(var.module_depends_on) + } +} + +resource "null_resource" "complete" { + depends_on = [google_compute_network_peering.local_network_peering, google_compute_network_peering.peer_network_peering] +} diff --git a/modules/net-vpc-peering/outputs.tf b/modules/net-vpc-peering/outputs.tf new file mode 100644 index 000000000..92bdc536a --- /dev/null +++ b/modules/net-vpc-peering/outputs.tf @@ -0,0 +1,30 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "local_network_peering" { + description = "Network peering resource." + value = google_compute_network_peering.local_network_peering +} + +output "peer_network_peering" { + description = "Peer network peering resource." + value = google_compute_network_peering.peer_network_peering +} + +output "complete" { + description = "Output to be used as a module dependency." + value = null_resource.complete.id +} diff --git a/modules/net-vpc-peering/variables.tf b/modules/net-vpc-peering/variables.tf new file mode 100644 index 000000000..4cd255a9d --- /dev/null +++ b/modules/net-vpc-peering/variables.tf @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "prefix" { + description = "Name prefix for the network peerings" + type = string + default = "network-peering" +} + +variable "local_network" { + description = "Resource link of the network to add a peering to." + type = string +} + +variable "peer_network" { + description = "Resource link of the peer network." + type = string +} + +variable "export_peer_custom_routes" { + description = "Export custom routes to local network from peer network." + type = bool + default = false +} + +variable "export_local_custom_routes" { + description = "Export custom routes to peer network from local network." + type = bool + default = false +} + +variable "module_depends_on" { + description = "List of modules or resources this module depends on." + type = list + default = [] +} diff --git a/modules/net-vpc-peering/versions.tf b/modules/net-vpc-peering/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/net-vpc-peering/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/net-vpc/README.md b/modules/net-vpc/README.md new file mode 100644 index 000000000..082650252 --- /dev/null +++ b/modules/net-vpc/README.md @@ -0,0 +1,141 @@ +# Minimalistic VPC module + +This module allows creation and management of VPC networks including subnetworks and subnetwork IAM bindings, Shared VPC activation and service project registration, and one-to-one peering. + +## Examples + +The module allows for several different VPC configurations, some of the most common are shown below. + +### Simple VPC + +```hcl +module "vpc" { + source = "../modules/net-vpc" + project_id = "my-project" + name = "my-network" + subnets = { + subnet-1 = { + ip_cidr_range = "10.0.0.0/24" + region = "europe-west1" + secondary_ip_range = { + pods = "172.16.0.0/20" + services = "192.168.0.0/24" + } + } + subnet-2 = { + ip_cidr_range = "10.0.16.0/24" + region = "europe-west1" + secondary_ip_range = {} + } + } +} +``` + +### Peering + +A single peering can be configured for the VPC, so as to allow management of simple scenarios, and more complex configurations like hub and spoke by defining the peering configuration on the spoke VPCs. Care must be taken so as a single peering is created/changed/destroyed at a time, due to the specific behaviour of the peering API calls. + +```hcl +module "vpc-spoke-1" { + source = "../modules/net-vpc" + project_id = "my-project" + name = "my-network" + subnets = { + subnet-1 = { + ip_cidr_range = "10.0.0.0/24" + region = "europe-west1" + secondary_ip_range = { + pods = "172.16.0.0/20" + services = "192.168.0.0/24" + } + } + } + peering_config = { + peer_vpc_self_link = module.vpc-hub.self_link + export_routes = false + import_routes = true + } +} +``` + +### Shared VPC + +```hcl +module "vpc-host" { + source = "../modules/net-vpc" + project_id = "my-project" + name = "my-host-network" + subnets = { + subnet-1 = { + ip_cidr_range = "10.0.0.0/24" + region = "europe-west1" + secondary_ip_range = { + pods = "172.16.0.0/20" + services = "192.168.0.0/24" + } + } + } + shared_vpc_host = true + shared_vpc_service_projects = [ + local.service_project_1.project_id, + local.service_project_2.project_id + ] + iam_roles = { + subnet-1 = [ + "roles/compute.networkUser", + "roles/compute.securityAdmin" + ] + } + iam_members = { + subnet-1 = { + "roles/compute.networkUser" = [ + local.service_project_1.cloudsvc_sa, + local.service_project_1.gke_sa + ] + "roles/compute.securityAdmin" = [ + local.service_project_1.gke_sa + ] + } + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| name | The name of the network being created | string | ✓ | | +| project_id | The ID of the project where this VPC will be created | string | ✓ | | +| *auto_create_subnetworks* | Set to true to create an auto mode subnet, defaults to custom mode. | bool | | false | +| *description* | An optional description of this resource (triggers recreation on change). | string | | Terraform-managed. | +| *iam_members* | List of IAM members keyed by subnet and role. | map(map(list(string))) | | null | +| *iam_roles* | List of IAM roles keyed by subnet. | map(list(string)) | | null | +| *log_config_defaults* | Default configuration for flow logs when enabled. | object({...}) | | ... | +| *log_configs* | Map of per-subnet optional configurations for flow logs when enabled. | map(map(string)) | | null | +| *peering_config* | VPC peering configuration. | object({...}) | | null | +| *routes* | Network routes, keyed by name. | map(object({...})) | | null | +| *routing_mode* | The network routing mode (default 'GLOBAL') | string | | GLOBAL | +| *shared_vpc_host* | Enable shared VPC for this project. | bool | | false | +| *shared_vpc_service_projects* | Shared VPC service projects to register with this host | list(string) | | [] | +| *subnet_descriptions* | Optional map of subnet descriptions, keyed by subnet name. | map(string) | | {} | +| *subnet_flow_logs* | Optional map of boolean to control flow logs (default is disabled), keyed by subnet name. | map(bool) | | {} | +| *subnet_private_access* | Optional map of boolean to control private Google access (default is enabled), keyed by subnet name. | map(bool) | | {} | +| *subnets* | The list of subnets being created | map(object({...})) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| bindings | Subnet IAM bindings. | | +| name | The name of the VPC being created. | | +| network | Network resource. | | +| project_id | Shared VPC host project id. | | +| self_link | The URI of the VPC being created. | | +| subnet_ips | Map of subnet address ranges keyed by name. | | +| subnet_regions | Map of subnet regions keyed by name. | | +| subnet_secondary_ranges | Map of subnet secondary ranges keyed by name. | | +| subnet_self_links | Map of subnet self links keyed by name. | | +| subnets | Subnet resources. | | + + diff --git a/modules/net-vpc/main.tf b/modules/net-vpc/main.tf new file mode 100644 index 000000000..f94cc5954 --- /dev/null +++ b/modules/net-vpc/main.tf @@ -0,0 +1,207 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + iam_members = var.iam_members == null ? {} : var.iam_members + iam_pairs = var.iam_roles == null ? [] : flatten([ + for subnet, roles in var.iam_roles : + [for role in roles : { subnet = subnet, role = role }] + ]) + iam_keypairs = { + for pair in local.iam_pairs : + "${pair.subnet}-${pair.role}" => pair + } + log_configs = var.log_configs == null ? {} : var.log_configs + peer_network = ( + var.peering_config == null + ? null + : element(reverse(split("/", var.peering_config.peer_vpc_self_link)), 0) + ) + routes = var.routes == null ? {} : var.routes + routes_gateway = { + for name, data in local.routes : + name => data if data.next_hop_type == "gateway" + } + routes_ilb = { + for name, data in local.routes : + name => data if data.next_hop_type == "ilb" + } + routes_instance = { + for name, data in local.routes : + name => data if data.next_hop_type == "instance" + } + routes_ip = { + for name, data in local.routes : + name => data if data.next_hop_type == "ip" + } + routes_vpn_tunnel = { + for name, data in local.routes : + name => data if data.next_hop_type == "vpn_tunnel" + } + subnet_log_configs = { + for name, attrs in local.subnets : name => ( + lookup(var.subnet_flow_logs, name, false) + ? [{ + for key, value in var.log_config_defaults : key => lookup( + lookup(local.log_configs, name, {}), key, value + ) + }] + : [] + ) + } + subnets = var.subnets == null ? {} : var.subnets +} + +resource "google_compute_network" "network" { + project = var.project_id + name = var.name + description = var.description + auto_create_subnetworks = var.auto_create_subnetworks + routing_mode = var.routing_mode +} + +resource "google_compute_network_peering" "local" { + provider = google-beta + count = var.peering_config == null ? 0 : 1 + name = "${google_compute_network.network.name}-${local.peer_network}" + network = google_compute_network.network.self_link + peer_network = var.peering_config.peer_vpc_self_link + export_custom_routes = var.peering_config.export_routes + import_custom_routes = var.peering_config.import_routes +} + +resource "google_compute_network_peering" "remote" { + provider = google-beta + count = var.peering_config == null ? 0 : 1 + name = "${local.peer_network}-${google_compute_network.network.name}" + network = var.peering_config.peer_vpc_self_link + peer_network = google_compute_network.network.self_link + export_custom_routes = var.peering_config.import_routes + import_custom_routes = var.peering_config.export_routes + depends_on = [google_compute_network_peering.local] +} + +resource "google_compute_shared_vpc_host_project" "shared_vpc_host" { + count = var.shared_vpc_host ? 1 : 0 + project = var.project_id + depends_on = [google_compute_network.network] +} + +resource "google_compute_shared_vpc_service_project" "service_projects" { + for_each = ( + var.shared_vpc_host && var.shared_vpc_service_projects != null + ? toset(var.shared_vpc_service_projects) + : toset([]) + ) + host_project = var.project_id + service_project = each.value + depends_on = [google_compute_shared_vpc_host_project.shared_vpc_host] +} + +resource "google_compute_subnetwork" "subnetwork" { + for_each = local.subnets + project = var.project_id + network = google_compute_network.network.name + region = each.value.region + name = "${var.name}-${each.key}" + ip_cidr_range = each.value.ip_cidr_range + secondary_ip_range = each.value.secondary_ip_range == null ? [] : [ + for name, range in each.value.secondary_ip_range : + { range_name = name, ip_cidr_range = range } + ] + description = lookup(var.subnet_descriptions, each.key, "Terraform-managed.") + private_ip_google_access = lookup(var.subnet_private_access, each.key, true) + dynamic "log_config" { + for_each = local.subnet_log_configs[each.key] + iterator = config + content { + aggregation_interval = config.value.aggregation_interval + flow_sampling = config.value.flow_sampling + metadata = config.value.metadata + } + } +} + +resource "google_compute_subnetwork_iam_binding" "binding" { + for_each = local.iam_keypairs + project = var.project_id + subnetwork = google_compute_subnetwork.subnetwork[each.value.subnet].name + region = google_compute_subnetwork.subnetwork[each.value.subnet].region + role = each.value.role + members = lookup( + lookup(local.iam_members, each.value.subnet, {}), each.value.role, [] + ) +} + +resource "google_compute_route" "gateway" { + for_each = local.routes_gateway + project = var.project_id + network = google_compute_network.network.name + name = each.key + description = "Terraform-managed." + dest_range = each.value.dest_range + priority = each.value.priority + tags = each.value.tags + next_hop_gateway = each.value.next_hop +} + +resource "google_compute_route" "ilb" { + for_each = local.routes_ilb + project = var.project_id + network = google_compute_network.network.name + name = each.key + description = "Terraform-managed." + dest_range = each.value.dest_range + priority = each.value.priority + tags = each.value.tags + next_hop_ilb = each.value.next_hop +} + +resource "google_compute_route" "instance" { + for_each = local.routes_instance + project = var.project_id + network = google_compute_network.network.name + name = each.key + description = "Terraform-managed." + dest_range = each.value.dest_range + priority = each.value.priority + tags = each.value.tags + next_hop_instance = each.value.next_hop +} + +resource "google_compute_route" "ip" { + for_each = local.routes_ip + project = var.project_id + network = google_compute_network.network.name + name = each.key + description = "Terraform-managed." + dest_range = each.value.dest_range + priority = each.value.priority + tags = each.value.tags + next_hop_ip = each.value.next_hop +} + +resource "google_compute_route" "vpn_tunnel" { + for_each = local.routes_vpn_tunnel + project = var.project_id + network = google_compute_network.network.name + name = each.key + description = "Terraform-managed." + dest_range = each.value.dest_range + priority = each.value.priority + tags = each.value.tags + next_hop_vpn_tunnel = each.value.next_hop +} diff --git a/modules/net-vpc/outputs.tf b/modules/net-vpc/outputs.tf new file mode 100644 index 000000000..64649135e --- /dev/null +++ b/modules/net-vpc/outputs.tf @@ -0,0 +1,80 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "network" { + description = "Network resource." + value = google_compute_network.network +} + +output "name" { + description = "The name of the VPC being created." + value = google_compute_network.network.name +} + +output "self_link" { + description = "The URI of the VPC being created." + value = google_compute_network.network.self_link +} + +output "project_id" { + description = "Shared VPC host project id." + value = ( + var.shared_vpc_host + ? google_compute_shared_vpc_host_project.shared_vpc_host[*].project + : null + ) + depends_on = [ + google_compute_shared_vpc_host_project.shared_vpc_host, + google_compute_shared_vpc_service_project.service_projects + ] +} + +# TODO(ludoo): use input names as keys +output "subnets" { + description = "Subnet resources." + value = { for k, v in google_compute_subnetwork.subnetwork : k => v } +} + +output "subnet_ips" { + description = "Map of subnet address ranges keyed by name." + value = { for k, v in google_compute_subnetwork.subnetwork : k => v.ip_cidr_range } +} + +output "subnet_self_links" { + description = "Map of subnet self links keyed by name." + value = { for k, v in google_compute_subnetwork.subnetwork : k => v.self_link } +} + +output "subnet_regions" { + description = "Map of subnet regions keyed by name." + value = { for k, v in google_compute_subnetwork.subnetwork : k => v.region } +} + +output "subnet_secondary_ranges" { + description = "Map of subnet secondary ranges keyed by name." + value = { + for k, v in google_compute_subnetwork.subnetwork : + k => { + for range in v.secondary_ip_range : + range.range_name => range.ip_cidr_range + } + } +} + +output "bindings" { + description = "Subnet IAM bindings." + value = { for k, v in google_compute_subnetwork_iam_binding.binding : k => v } +} diff --git a/modules/net-vpc/variables.tf b/modules/net-vpc/variables.tf new file mode 100644 index 000000000..1f4513211 --- /dev/null +++ b/modules/net-vpc/variables.tf @@ -0,0 +1,137 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "auto_create_subnetworks" { + description = "Set to true to create an auto mode subnet, defaults to custom mode." + type = bool + default = false +} + +variable "description" { + description = "An optional description of this resource (triggers recreation on change)." + type = string + default = "Terraform-managed." +} + +variable "iam_roles" { + description = "List of IAM roles keyed by subnet." + type = map(list(string)) + default = null +} + +variable "iam_members" { + description = "List of IAM members keyed by subnet and role." + type = map(map(list(string))) + default = null +} + +variable "log_configs" { + description = "Map of per-subnet optional configurations for flow logs when enabled." + type = map(map(string)) + default = null +} + +variable "log_config_defaults" { + description = "Default configuration for flow logs when enabled." + type = object({ + aggregation_interval = string + flow_sampling = number + metadata = string + }) + default = { + aggregation_interval = "INTERVAL_5_SEC" + flow_sampling = 0.5 + metadata = "INCLUDE_ALL_METADATA" + } +} + +variable "name" { + description = "The name of the network being created" + type = string +} + +variable "peering_config" { + description = "VPC peering configuration." + type = object({ + peer_vpc_self_link = string + export_routes = bool + import_routes = bool + }) + default = null +} + +variable "project_id" { + description = "The ID of the project where this VPC will be created" + type = string +} + +variable "routes" { + description = "Network routes, keyed by name." + type = map(object({ + dest_range = string + priority = number + tags = list(string) + next_hop_type = string # gateway, instance, ip, vpn_tunnel, ilb + next_hop = string + })) + default = null +} + +variable "routing_mode" { + description = "The network routing mode (default 'GLOBAL')" + type = string + default = "GLOBAL" +} + +variable "shared_vpc_host" { + description = "Enable shared VPC for this project." + type = bool + default = false +} + +variable "shared_vpc_service_projects" { + description = "Shared VPC service projects to register with this host" + type = list(string) + default = [] +} + +variable "subnets" { + description = "The list of subnets being created" + type = map(object({ + ip_cidr_range = string + region = string + secondary_ip_range = map(string) + })) + default = null +} + +variable "subnet_descriptions" { + description = "Optional map of subnet descriptions, keyed by subnet name." + type = map(string) + default = {} +} + +variable "subnet_flow_logs" { + description = "Optional map of boolean to control flow logs (default is disabled), keyed by subnet name." + type = map(bool) + default = {} +} + +variable "subnet_private_access" { + description = "Optional map of boolean to control private Google access (default is enabled), keyed by subnet name." + type = map(bool) + default = {} +} diff --git a/modules/net-vpc/versions.tf b/modules/net-vpc/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/net-vpc/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/net-vpn-dynamic/README.md b/modules/net-vpn-dynamic/README.md new file mode 100644 index 000000000..218af8b14 --- /dev/null +++ b/modules/net-vpn-dynamic/README.md @@ -0,0 +1,72 @@ +# Cloud VPN Dynamic Module + +## Example + +This example shows how to configure a single VPN tunnel using a couple of extra features + +- custom advertisement on the tunnel's BGP session; if custom advertisement is not needed, simply set the `bgp_peer_options` attribute to `null` +- internally generated shared secret, which can be fetched from the module's `random_secret` output for reuse; a predefined secret can be used instead by assigning it to the `shared_secret` attribute + +```hcl +module "vpn-dynamic" { + source = "./modules/net-vpn-dynamic" + project_id = "my-project" + region = "europe-west1" + network = "my-vpc" + name = "gateway-1" + tunnels = { + remote-1 = { + bgp_peer = { + address = "169.254.139.134" + asn = 64513 + } + bgp_session_range = "169.254.139.133/30" + ike_version = 2 + peer_ip = var.remote_vpn_gateway.address + shared_secret = null + bgp_peer_options = { + advertise_groups = ["ALL_SUBNETS"] + advertise_ip_ranges = { + "192.168.0.0/24" = "Advertised range description" + } + advertise_mode = "CUSTOM" + route_priority = 1000 + } + } + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| name | VPN gateway name, and prefix used for dependent resources. | string | ✓ | | +| network | VPC used for the gateway and routes. | string | ✓ | | +| project_id | Project where resources will be created. | string | ✓ | | +| region | Region used for resources. | string | ✓ | | +| *gateway_address* | Optional address assigned to the VPN gateway. Ignored unless gateway_address_create is set to false. | string | | | +| *gateway_address_create* | Create external address assigned to the VPN gateway. Needs to be explicitly set to false to use address in gateway_address variable. | bool | | true | +| *route_priority* | Route priority, defaults to 1000. | number | | 1000 | +| *router_advertise_config* | Router custom advertisement configuration, ip_ranges is a map of address ranges and descriptions. | object({...}) | | null | +| *router_asn* | Router ASN used for auto-created router. | number | | 64514 | +| *router_create* | Create router. | bool | | true | +| *router_name* | Router name used for auto created router, or to specify existing router to use. Leave blank to use VPN name for auto created router. | string | | | +| *tunnels* | VPN tunnel configurations, bgp_peer_options is usually null. | map(object({...})) | | {} | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| address | VPN gateway address. | | +| gateway | VPN gateway resource. | | +| name | VPN gateway name. | | +| random_secret | Generated secret. | ✓ | +| router | Router resource (only if auto-created). | | +| router_name | Router name. | | +| self_link | VPN gateway self link. | | +| tunnel_names | VPN tunnel names. | | +| tunnel_self_links | VPN tunnel self links. | | +| tunnels | VPN tunnel resources. | | + diff --git a/modules/net-vpn-dynamic/main.tf b/modules/net-vpn-dynamic/main.tf new file mode 100644 index 000000000..368b3abe0 --- /dev/null +++ b/modules/net-vpn-dynamic/main.tf @@ -0,0 +1,182 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + gateway_address = ( + var.gateway_address_create + ? google_compute_address.gateway[0].address + : var.gateway_address + ) + router = ( + var.router_create + ? google_compute_router.router[0].name + : var.router_name + ) + secret = random_id.secret.b64_url +} + +resource "google_compute_address" "gateway" { + count = var.gateway_address_create ? 1 : 0 + name = "vpn-${var.name}" + project = var.project_id + region = var.region +} + +resource "google_compute_forwarding_rule" "esp" { + name = "vpn-${var.name}-esp" + project = var.project_id + region = var.region + target = google_compute_vpn_gateway.gateway.self_link + ip_address = local.gateway_address + ip_protocol = "ESP" +} + +resource "google_compute_forwarding_rule" "udp-500" { + name = "vpn-${var.name}-udp-500" + project = var.project_id + region = var.region + target = google_compute_vpn_gateway.gateway.self_link + ip_address = local.gateway_address + ip_protocol = "UDP" + port_range = "500" +} + +resource "google_compute_forwarding_rule" "udp-4500" { + name = "vpn-${var.name}-udp-4500" + project = var.project_id + region = var.region + target = google_compute_vpn_gateway.gateway.self_link + ip_address = local.gateway_address + ip_protocol = "UDP" + port_range = "4500" +} + +resource "google_compute_router" "router" { + count = var.router_create ? 1 : 0 + name = var.router_name == "" ? "vpn-${var.name}" : var.router_name + project = var.project_id + region = var.region + network = var.network + bgp { + advertise_mode = ( + var.router_advertise_config == null + ? null + : var.router_advertise_config.mode + ) + advertised_groups = ( + var.router_advertise_config == null ? null : ( + var.router_advertise_config.mode != "CUSTOM" + ? null + : var.router_advertise_config.groups + ) + ) + dynamic advertised_ip_ranges { + for_each = ( + var.router_advertise_config == null ? {} : ( + var.router_advertise_config.mode != "CUSTOM" + ? null + : var.router_advertise_config.ip_ranges + ) + ) + iterator = range + content { + range = range.key + description = range.value + } + } + asn = var.router_asn + } +} + +resource "google_compute_router_peer" "bgp_peer" { + for_each = var.tunnels + region = var.region + project = var.project_id + name = "${var.name}-${each.key}" + router = local.router + peer_ip_address = each.value.bgp_peer.address + peer_asn = each.value.bgp_peer.asn + advertised_route_priority = ( + each.value.bgp_peer_options == null ? var.route_priority : ( + each.value.bgp_peer_options.route_priority == null + ? var.route_priority + : each.value.bgp_peer_options.route_priority + ) + ) + advertise_mode = ( + each.value.bgp_peer_options == null ? null : each.value.bgp_peer_options.advertise_mode + ) + advertised_groups = ( + each.value.bgp_peer_options == null ? null : ( + each.value.bgp_peer_options.advertise_mode != "CUSTOM" + ? null + : each.value.bgp_peer_options.advertise_groups + ) + ) + dynamic advertised_ip_ranges { + for_each = ( + each.value.bgp_peer_options == null ? {} : ( + each.value.bgp_peer_options.advertise_mode != "CUSTOM" + ? {} + : each.value.bgp_peer_options.advertise_ip_ranges + ) + ) + iterator = range + content { + range = range.key + description = range.value + } + } + interface = google_compute_router_interface.router_interface[each.key].name +} + +resource "google_compute_router_interface" "router_interface" { + for_each = var.tunnels + project = var.project_id + region = var.region + name = "${var.name}-${each.key}" + router = local.router + ip_range = each.value.bgp_session_range == "" ? null : each.value.bgp_session_range + vpn_tunnel = google_compute_vpn_tunnel.tunnels[each.key].name +} + +resource "google_compute_vpn_gateway" "gateway" { + name = var.name + project = var.project_id + region = var.region + network = var.network +} + +resource "google_compute_vpn_tunnel" "tunnels" { + for_each = var.tunnels + project = var.project_id + region = var.region + name = "${var.name}-${each.key}" + router = local.router + peer_ip = each.value.peer_ip + ike_version = each.value.ike_version + shared_secret = ( + each.value.shared_secret == "" || each.value.shared_secret == null + ? local.secret + : each.value.shared_secret + ) + target_vpn_gateway = google_compute_vpn_gateway.gateway.self_link + depends_on = [google_compute_forwarding_rule.esp] +} + +resource "random_id" "secret" { + byte_length = 8 +} diff --git a/modules/net-vpn-dynamic/outputs.tf b/modules/net-vpn-dynamic/outputs.tf new file mode 100644 index 000000000..78e435f3f --- /dev/null +++ b/modules/net-vpn-dynamic/outputs.tf @@ -0,0 +1,75 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "address" { + description = "VPN gateway address." + value = local.gateway_address +} + +output "gateway" { + description = "VPN gateway resource." + value = google_compute_vpn_gateway.gateway +} + +output "name" { + description = "VPN gateway name." + value = google_compute_vpn_gateway.gateway.name +} + +output "router" { + description = "Router resource (only if auto-created)." + value = var.router_create ? google_compute_router.router[0] : null +} + +output "router_name" { + description = "Router name." + value = local.router +} + +output "self_link" { + description = "VPN gateway self link." + value = google_compute_vpn_gateway.gateway.self_link +} + +output "tunnels" { + description = "VPN tunnel resources." + value = { + for name in keys(var.tunnels) : + name => google_compute_vpn_tunnel.tunnels[name] + } +} + +output "tunnel_names" { + description = "VPN tunnel names." + value = { + for name in keys(var.tunnels) : + name => google_compute_vpn_tunnel.tunnels[name].name + } +} + +output "tunnel_self_links" { + description = "VPN tunnel self links." + value = { + for name in keys(var.tunnels) : + name => google_compute_vpn_tunnel.tunnels[name].self_link + } +} + +output "random_secret" { + description = "Generated secret." + sensitive = true + value = local.secret +} diff --git a/modules/net-vpn-dynamic/variables.tf b/modules/net-vpn-dynamic/variables.tf new file mode 100644 index 000000000..eea77c00b --- /dev/null +++ b/modules/net-vpn-dynamic/variables.tf @@ -0,0 +1,104 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "gateway_address_create" { + description = "Create external address assigned to the VPN gateway. Needs to be explicitly set to false to use address in gateway_address variable." + type = bool + default = true +} + +variable "gateway_address" { + description = "Optional address assigned to the VPN gateway. Ignored unless gateway_address_create is set to false." + type = string + default = "" +} + +variable "name" { + description = "VPN gateway name, and prefix used for dependent resources." + type = string +} + +variable "network" { + description = "VPC used for the gateway and routes." + type = string +} + +variable "project_id" { + description = "Project where resources will be created." + type = string +} + +variable "region" { + description = "Region used for resources." + type = string +} + +variable "route_priority" { + description = "Route priority, defaults to 1000." + type = number + default = 1000 +} + +variable "router_advertise_config" { + description = "Router custom advertisement configuration, ip_ranges is a map of address ranges and descriptions." + type = object({ + groups = list(string) + ip_ranges = map(string) + mode = string + }) + default = null +} + +variable "router_asn" { + description = "Router ASN used for auto-created router." + type = number + default = 64514 +} + +variable "router_create" { + description = "Create router." + type = bool + default = true +} + +variable "router_name" { + description = "Router name used for auto created router, or to specify existing router to use. Leave blank to use VPN name for auto created router." + type = string + default = "" +} + +variable "tunnels" { + description = "VPN tunnel configurations, bgp_peer_options is usually null." + type = map(object({ + bgp_peer = object({ + address = string + asn = number + }) + bgp_peer_options = object({ + advertise_groups = list(string) + advertise_ip_ranges = map(string) + advertise_mode = string + route_priority = number + }) + # each BGP session on the same Cloud Router must use a unique /30 CIDR + # from the 169.254.0.0/16 block. + bgp_session_range = string + ike_version = number + peer_ip = string + shared_secret = string + })) + default = {} +} diff --git a/modules/net-vpn-dynamic/versions.tf b/modules/net-vpn-dynamic/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/net-vpn-dynamic/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/net-vpn-ha/README.md b/modules/net-vpn-ha/README.md new file mode 100644 index 000000000..d5fffa635 --- /dev/null +++ b/modules/net-vpn-ha/README.md @@ -0,0 +1,159 @@ +# Cloud VPN HA Module +This module makes it easy to deploy either GCP-to-GCP or GCP-to-On-prem [Cloud HA VPN](https://cloud.google.com/vpn/docs/concepts/overview#ha-vpn). + +## Examples + +### GCP to GCP +```hcl +module "vpn_ha-1" { + source = "../modules/net-vpn-ha" + project_id = "" + region = "europe-west4" + network = "https://www.googleapis.com/compute/v1/projects//global/networks/network-1" + name = "net1-to-net-2" + peer_gcp_gateway = module.vpn_ha-2.self_link + router_asn = 64514 + tunnels = { + remote-0 = { + bgp_peer = { + address = "169.254.1.1" + asn = 64513 + } + bgp_peer_options = null + bgp_session_range = "169.254.1.2/30" + ike_version = 2 + vpn_gateway_interface = 0 + peer_external_gateway_interface = null + shared_secret = "" + } + remote-1 = { + bgp_peer = { + address = "169.254.2.1" + asn = 64513 + } + bgp_peer_options = null + bgp_session_range = "169.254.2.2/30" + ike_version = 2 + vpn_gateway_interface = 1 + peer_external_gateway_interface = null + shared_secret = "" + } + } +} + +module "vpn_ha-2" { + source = "../modules/net-vpn-ha" + project_id = "" + region = "europe-west4" + network = "https://www.googleapis.com/compute/v1/projects//global/networks/local-network" + name = "net2-to-net1" + router_asn = 64513 + peer_gcp_gateway = module.vpn_ha-1.self_link + tunnels = { + remote-0 = { + bgp_peer = { + address = "169.254.1.2" + asn = 64514 + } + bgp_peer_options = null + bgp_session_range = "169.254.1.1/30" + ike_version = 2 + vpn_gateway_interface = 0 + peer_external_gateway_interface = null + shared_secret = module.vpn_ha-1.random_secret + } + remote-1 = { + bgp_peer = { + address = "169.254.2.2" + asn = 64514 + } + bgp_peer_options = null + bgp_session_range = "169.254.2.1/30" + ike_version = 2 + vpn_gateway_interface = 1 + peer_external_gateway_interface = null + shared_secret = module.vpn_ha-1.random_secret + } + } +} +``` +### GCP to on-prem + +``` +module "vpn_ha" { + source = "../modules/net-vpn-ha" + project_id = "" + region = "europe-west4" + network = "https://www.googleapis.com/compute/v1/projects//global/networks/my-network" + name = "mynet-to-onprem" + peer_external_gateway = { + redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT" + interfaces = [{ + id = 0 + ip_address = "8.8.8.8" # on-prem router ip address + + }] + } + router_asn = 64514 + tunnels = { + remote-0 = { + bgp_peer = { + address = "169.254.1.1" + asn = 64513 + } + bgp_peer_options = null + bgp_session_range = "169.254.1.2/30" + ike_version = 2 + vpn_gateway_interface = 0 + peer_external_gateway_interface = 0 + shared_secret = "mySecret" + } + remote-1 = { + bgp_peer = { + address = "169.254.2.1" + asn = 64513 + } + bgp_peer_options = null + bgp_session_range = "169.254.2.2/30" + ike_version = 2 + vpn_gateway_interface = 1 + peer_external_gateway_interface = 0 + shared_secret = "mySecret" + } + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| name | VPN gateway name, and prefix used for dependent resources. | string | ✓ | | +| network | VPC used for the gateway and routes. | string | ✓ | | +| project_id | Project where resources will be created. | string | ✓ | | +| region | Region used for resources. | string | ✓ | | +| *peer_external_gateway* | Configuration of an external VPN gateway to which this VPN is connected. | object({...}) | | null | +| *peer_gcp_gateway* | Self Link URL of the peer side HA GCP VPN gateway to which this VPN tunnel is connected. | string | | null | +| *route_priority* | Route priority, defaults to 1000. | number | | 1000 | +| *router_advertise_config* | Router custom advertisement configuration, ip_ranges is a map of address ranges and descriptions. | object({...}) | | null | +| *router_asn* | Router ASN used for auto-created router. | number | | 64514 | +| *router_create* | Create router. | bool | | true | +| *router_name* | Router name used for auto created router, or to specify existing router to use. Leave blank to use VPN name for auto created router. | string | | | +| *tunnels* | VPN tunnel configurations, bgp_peer_options is usually null. | map(object({...})) | | {} | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| external_gateway | External VPN gateway resource. | | +| gateway | HA VPN gateway resource. | | +| name | VPN gateway name. | | +| random_secret | Generated secret. | ✓ | +| router | Router resource (only if auto-created). | | +| router_name | Router name. | | +| self_link | HA VPN gateway self link. | | +| tunnel_names | VPN tunnel names. | | +| tunnel_self_links | VPN tunnel self links. | | +| tunnels | VPN tunnel resources. | | + \ No newline at end of file diff --git a/modules/net-vpn-ha/main.tf b/modules/net-vpn-ha/main.tf new file mode 100644 index 000000000..49055b1b5 --- /dev/null +++ b/modules/net-vpn-ha/main.tf @@ -0,0 +1,169 @@ + +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + peer_external_gateway = ( + var.peer_external_gateway != null + ? google_compute_external_vpn_gateway.external_gateway[0].self_link + : null + + ) + router = ( + var.router_create + ? google_compute_router.router[0].name + : var.router_name + ) + secret = random_id.secret.b64_url +} + +resource "google_compute_ha_vpn_gateway" "ha_gateway" { + provider = google-beta + name = var.name + project = var.project_id + region = var.region + network = var.network +} + +resource "google_compute_external_vpn_gateway" "external_gateway" { + provider = google-beta + count = var.peer_external_gateway != null ? 1 : 0 + name = "external-${var.name}" + redundancy_type = var.peer_external_gateway.redundancy_type + description = "Terraform managed external VPN gateway" + dynamic "interface" { + for_each = var.peer_external_gateway.interfaces + content { + id = interface.value.id + ip_address = interface.value.ip_address + } + } +} + +resource "google_compute_router" "router" { + provider = google-beta + count = var.router_create ? 1 : 0 + name = var.router_name == "" ? "vpn-${var.name}" : var.router_name + project = var.project_id + region = var.region + network = var.network + bgp { + advertise_mode = ( + var.router_advertise_config == null + ? null + : var.router_advertise_config.mode + ) + advertised_groups = ( + var.router_advertise_config == null ? null : ( + var.router_advertise_config.mode != "CUSTOM" + ? null + : var.router_advertise_config.groups + ) + ) + dynamic advertised_ip_ranges { + for_each = ( + var.router_advertise_config == null ? {} : ( + var.router_advertise_config.mode != "CUSTOM" + ? null + : var.router_advertise_config.ip_ranges + ) + ) + iterator = range + content { + range = range.key + description = range.value + } + } + asn = var.router_asn + } +} + +resource "google_compute_router_peer" "bgp_peer" { + for_each = var.tunnels + region = var.region + project = var.project_id + name = "${var.name}-${each.key}" + router = local.router + peer_ip_address = each.value.bgp_peer.address + peer_asn = each.value.bgp_peer.asn + advertised_route_priority = ( + each.value.bgp_peer_options == null ? var.route_priority : ( + each.value.bgp_peer_options.route_priority == null + ? var.route_priority + : each.value.bgp_peer_options.route_priority + ) + ) + advertise_mode = ( + each.value.bgp_peer_options == null ? null : each.value.bgp_peer_options.advertise_mode + ) + advertised_groups = ( + each.value.bgp_peer_options == null ? null : ( + each.value.bgp_peer_options.advertise_mode != "CUSTOM" + ? null + : each.value.bgp_peer_options.advertise_groups + ) + ) + dynamic advertised_ip_ranges { + for_each = ( + each.value.bgp_peer_options == null ? {} : ( + each.value.bgp_peer_options.advertise_mode != "CUSTOM" + ? {} + : each.value.bgp_peer_options.advertise_ip_ranges + ) + ) + iterator = range + content { + range = range.key + description = range.value + } + } + interface = google_compute_router_interface.router_interface[each.key].name +} + +resource "google_compute_router_interface" "router_interface" { + provider = google-beta + for_each = var.tunnels + project = var.project_id + region = var.region + name = "${var.name}-${each.key}" + router = local.router + ip_range = each.value.bgp_session_range == "" ? null : each.value.bgp_session_range + vpn_tunnel = google_compute_vpn_tunnel.tunnels[each.key].name +} + +resource "google_compute_vpn_tunnel" "tunnels" { + provider = google-beta + for_each = var.tunnels + project = var.project_id + region = var.region + name = "${var.name}-${each.key}" + router = local.router + peer_external_gateway = local.peer_external_gateway + peer_external_gateway_interface = each.value.peer_external_gateway_interface + peer_gcp_gateway = var.peer_gcp_gateway + vpn_gateway_interface = each.value.vpn_gateway_interface + ike_version = each.value.ike_version + shared_secret = ( + each.value.shared_secret == "" || each.value.shared_secret == null + ? local.secret + : each.value.shared_secret + ) + vpn_gateway = google_compute_ha_vpn_gateway.ha_gateway.self_link +} + +resource "random_id" "secret" { + byte_length = 8 +} diff --git a/modules/net-vpn-ha/outputs.tf b/modules/net-vpn-ha/outputs.tf new file mode 100644 index 000000000..2a5388900 --- /dev/null +++ b/modules/net-vpn-ha/outputs.tf @@ -0,0 +1,80 @@ + +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "gateway" { + description = "HA VPN gateway resource." + value = google_compute_ha_vpn_gateway.ha_gateway +} + +output "external_gateway" { + description = "External VPN gateway resource." + value = ( + var.peer_external_gateway != null + ? google_compute_external_vpn_gateway.external_gateway[0] + : null + ) +} + +output "name" { + description = "VPN gateway name." + value = google_compute_ha_vpn_gateway.ha_gateway.name +} + +output "router" { + description = "Router resource (only if auto-created)." + value = var.router_name == "" ? google_compute_router.router[0] : null +} + +output "router_name" { + description = "Router name." + value = local.router +} + +output "self_link" { + description = "HA VPN gateway self link." + value = google_compute_ha_vpn_gateway.ha_gateway.self_link +} + +output "tunnels" { + description = "VPN tunnel resources." + value = { + for name in keys(var.tunnels) : + name => google_compute_vpn_tunnel.tunnels[name] + } +} + +output "tunnel_names" { + description = "VPN tunnel names." + value = { + for name in keys(var.tunnels) : + name => google_compute_vpn_tunnel.tunnels[name].name + } +} + +output "tunnel_self_links" { + description = "VPN tunnel self links." + value = { + for name in keys(var.tunnels) : + name => google_compute_vpn_tunnel.tunnels[name].self_link + } +} + +output "random_secret" { + description = "Generated secret." + sensitive = true + value = local.secret +} diff --git a/modules/net-vpn-ha/variables.tf b/modules/net-vpn-ha/variables.tf new file mode 100644 index 000000000..55f4ec899 --- /dev/null +++ b/modules/net-vpn-ha/variables.tf @@ -0,0 +1,111 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "name" { + description = "VPN gateway name, and prefix used for dependent resources." + type = string +} + +variable "network" { + description = "VPC used for the gateway and routes." + type = string +} + +variable "peer_external_gateway" { + description = "Configuration of an external VPN gateway to which this VPN is connected." + type = object({ + redundancy_type = string + interfaces = list(object({ + id = number + ip_address = string + })) + }) + default = null +} + +variable "peer_gcp_gateway" { + description = "Self Link URL of the peer side HA GCP VPN gateway to which this VPN tunnel is connected." + type = string + default = null +} + +variable "project_id" { + description = "Project where resources will be created." + type = string +} + +variable "region" { + description = "Region used for resources." + type = string +} + +variable "route_priority" { + description = "Route priority, defaults to 1000." + type = number + default = 1000 +} + +variable "router_advertise_config" { + description = "Router custom advertisement configuration, ip_ranges is a map of address ranges and descriptions." + type = object({ + groups = list(string) + ip_ranges = map(string) + mode = string + }) + default = null +} + +variable "router_asn" { + description = "Router ASN used for auto-created router." + type = number + default = 64514 +} + +variable "router_create" { + description = "Create router." + type = bool + default = true +} + +variable "router_name" { + description = "Router name used for auto created router, or to specify existing router to use. Leave blank to use VPN name for auto created router." + type = string + default = "" +} + +variable "tunnels" { + description = "VPN tunnel configurations, bgp_peer_options is usually null." + type = map(object({ + bgp_peer = object({ + address = string + asn = number + }) + bgp_peer_options = object({ + advertise_groups = list(string) + advertise_ip_ranges = map(string) + advertise_mode = string + route_priority = number + }) + # each BGP session on the same Cloud Router must use a unique /30 CIDR + # from the 169.254.0.0/16 block. + bgp_session_range = string + ike_version = number + vpn_gateway_interface = number + peer_external_gateway_interface = number + shared_secret = string + })) + default = {} +} diff --git a/modules/net-vpn-ha/versions.tf b/modules/net-vpn-ha/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/net-vpn-ha/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/net-vpn-static/README.md b/modules/net-vpn-static/README.md new file mode 100644 index 000000000..5961e8850 --- /dev/null +++ b/modules/net-vpn-static/README.md @@ -0,0 +1,52 @@ +# Cloud VPN Route-based Module + +## Example + +```hcl +module "vpn" { + source = "./modules/net-vpn-static" + project_id = var.project_id + region = var.region + network = var.network + name = "remote" + # gateway_address = var.gateway_address + remote_ranges = [var.remote_ranges] + tunnels = { + remote-0 = { + ike_version = 2 + peer_ip = var.remote_vpn_gateway_address + shared_secret = "" + traffic_selectors = { local = ["0.0.0.0/0"], remote = null } + } + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| name | VPN gateway name, and prefix used for dependent resources. | string | ✓ | | +| network | VPC used for the gateway and routes. | string | ✓ | | +| project_id | Project where resources will be created. | string | ✓ | | +| region | Region used for resources. | string | ✓ | | +| *gateway_address* | Optional address assigned to the VPN gateway. Ignored unless gateway_address_create is set to false. | string | | | +| *gateway_address_create* | Create external address assigned to the VPN gateway. Needs to be explicitly set to false to use address in gateway_address variable. | bool | | true | +| *remote_ranges* | Remote IP CIDR ranges. | list(string) | | [] | +| *route_priority* | Route priority, defaults to 1000. | number | | 1000 | +| *tunnels* | VPN tunnel configurations. | map(object({...})) | | {} | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| address | VPN gateway address. | | +| gateway | VPN gateway resource. | | +| name | VPN gateway name. | | +| random_secret | Generated secret. | ✓ | +| self_link | VPN gateway self link. | | +| tunnel_names | VPN tunnel names. | | +| tunnel_self_links | VPN tunnel self links. | | +| tunnels | VPN tunnel resources. | | + diff --git a/modules/net-vpn-static/main.tf b/modules/net-vpn-static/main.tf new file mode 100644 index 000000000..559d8d959 --- /dev/null +++ b/modules/net-vpn-static/main.tf @@ -0,0 +1,101 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + gateway_address = ( + var.gateway_address_create + ? google_compute_address.gateway[0].address + : var.gateway_address + ) + route_pairs = { + for pair in setproduct(keys(var.tunnels), var.remote_ranges) : + "${pair[0]}-${join("-", regexall("[0-9]+", pair[1]))}" => { + tunnel = pair[0], range = pair[1] + } + } + secret = random_id.secret.b64_url +} + +resource "google_compute_address" "gateway" { + count = var.gateway_address_create ? 1 : 0 + name = "vpn-${var.name}" + project = var.project_id + region = var.region +} + +resource "google_compute_forwarding_rule" "esp" { + name = "vpn-${var.name}-esp" + project = var.project_id + region = var.region + target = google_compute_vpn_gateway.gateway.self_link + ip_address = local.gateway_address + ip_protocol = "ESP" +} + +resource "google_compute_forwarding_rule" "udp-500" { + name = "vpn-${var.name}-udp-500" + project = var.project_id + region = var.region + target = google_compute_vpn_gateway.gateway.self_link + ip_address = local.gateway_address + ip_protocol = "UDP" + port_range = "500" +} + +resource "google_compute_forwarding_rule" "udp-4500" { + name = "vpn-${var.name}-udp-4500" + project = var.project_id + region = var.region + target = google_compute_vpn_gateway.gateway.self_link + ip_address = local.gateway_address + ip_protocol = "UDP" + port_range = "4500" +} + +resource "google_compute_route" "route" { + for_each = local.route_pairs + name = "vpn-${each.key}" + project = var.project_id + network = var.network + dest_range = each.value.range + priority = var.route_priority + next_hop_vpn_tunnel = google_compute_vpn_tunnel.tunnels[each.value.tunnel].self_link +} + +resource "google_compute_vpn_gateway" "gateway" { + name = var.name + project = var.project_id + region = var.region + network = var.network +} + +resource "google_compute_vpn_tunnel" "tunnels" { + for_each = var.tunnels + name = "${var.name}-${each.key}" + project = var.project_id + region = var.region + peer_ip = each.value.peer_ip + local_traffic_selector = each.value.traffic_selectors.local + remote_traffic_selector = each.value.traffic_selectors.remote + ike_version = each.value.ike_version + shared_secret = each.value.shared_secret == "" ? local.secret : each.value.shared_secret + target_vpn_gateway = google_compute_vpn_gateway.gateway.self_link + depends_on = [google_compute_forwarding_rule.esp] +} + +resource "random_id" "secret" { + byte_length = 8 +} diff --git a/modules/net-vpn-static/outputs.tf b/modules/net-vpn-static/outputs.tf new file mode 100644 index 000000000..09ecc5ec0 --- /dev/null +++ b/modules/net-vpn-static/outputs.tf @@ -0,0 +1,65 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "address" { + description = "VPN gateway address." + value = local.gateway_address +} + +output "gateway" { + description = "VPN gateway resource." + value = google_compute_vpn_gateway.gateway +} + +output "name" { + description = "VPN gateway name." + value = google_compute_vpn_gateway.gateway.name +} + +output "self_link" { + description = "VPN gateway self link." + value = google_compute_vpn_gateway.gateway.self_link +} + +output "tunnels" { + description = "VPN tunnel resources." + value = { + for name in keys(var.tunnels) : + name => google_compute_vpn_tunnel.tunnels[name] + } +} + +output "tunnel_names" { + description = "VPN tunnel names." + value = { + for name in keys(var.tunnels) : + name => google_compute_vpn_tunnel.tunnels[name].name + } +} + +output "tunnel_self_links" { + description = "VPN tunnel self links." + value = { + for name in keys(var.tunnels) : + name => google_compute_vpn_tunnel.tunnels[name].self_link + } +} + +output "random_secret" { + description = "Generated secret." + sensitive = true + value = local.secret +} diff --git a/modules/net-vpn-static/variables.tf b/modules/net-vpn-static/variables.tf new file mode 100644 index 000000000..f3cebacc5 --- /dev/null +++ b/modules/net-vpn-static/variables.tf @@ -0,0 +1,73 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "gateway_address_create" { + description = "Create external address assigned to the VPN gateway. Needs to be explicitly set to false to use address in gateway_address variable." + type = bool + default = true +} + +variable "gateway_address" { + description = "Optional address assigned to the VPN gateway. Ignored unless gateway_address_create is set to false." + type = string + default = "" +} + +variable "name" { + description = "VPN gateway name, and prefix used for dependent resources." + type = string +} + +variable "network" { + description = "VPC used for the gateway and routes." + type = string +} + +variable "project_id" { + description = "Project where resources will be created." + type = string +} + +variable "region" { + description = "Region used for resources." + type = string +} + +variable "remote_ranges" { + description = "Remote IP CIDR ranges." + type = list(string) + default = [] +} + +variable "route_priority" { + description = "Route priority, defaults to 1000." + type = number + default = 1000 +} + +variable "tunnels" { + description = "VPN tunnel configurations." + type = map(object({ + ike_version = number + peer_ip = string + shared_secret = string + traffic_selectors = object({ + local = list(string) + remote = list(string) + }) + })) + default = {} +} diff --git a/modules/net-vpn-static/versions.tf b/modules/net-vpn-static/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/net-vpn-static/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/modules/on-prem-in-a-box/README.md b/modules/on-prem-in-a-box/README.md new file mode 100644 index 000000000..52098bb1c --- /dev/null +++ b/modules/on-prem-in-a-box/README.md @@ -0,0 +1,131 @@ +# On-prem-in-a-box Module + +This module allows emulating an on-premise enviroment in a single GCE VM, by connecting a Docker Network to a VPC via a static or dynamic (BGP) VPN connection implemented with Strongswan. It provides a good playground for testing private access and hybrid DNS connectivity between on-premise and Google Cloud. + +To see this module in action, please refer to the folowing end-to-end network examples: +- [hub-and-spoke-peerings](../../infrastructure/hub-and-spoke-peerings/) + +## TODO + +- [ ] describe how to check and troubleshoot the onprem VPN and services +- [ ] add support for service account, scopes and network tags +- [ ] allow passing in arbitrary CoreDNS configurations instead of tweaking a default one via variables + +## Examples + +### Static VPN Gateway +```hcl +module "cloud-vpn" { + source = "modules/net-vpn-static/" + project_id = "" + region = "europe-west4" + network = "vpn-network" + name = "cloud-net-to-on-prem" + remote_ranges = ["192.168.192.0/24"] + tunnels = { + remote-0 = { + ike_version = 2 + peer_ip = module.on-prem.external_address + shared_secret = "" + traffic_selectors = { local = ["0.0.0.0/0"], remote = null } + } + } +} + +module "on-prem" { + source = "modules/on-prem-in-a-box/" + + name = "onprem-instance" + project_id = "" + zone = "europe-west4-b" + network = + subnet_self_link = "https://www.googleapis.com/compute/v1/projects//regions/europe-west4/subnetworks/" + vpn_gateway_type = "static" + peer_ip = module.cloud-vpn.address + local_ip_cidr_range = "192.168.192.0/24" + shared_secret = module.cloud-vpn.random_secret + remote_ip_cidr_ranges = "172.16.0.0/24,172.16.1.0/24,172.16.2.0/24" +} +``` + +### Dynamic VPN Gateway +```hcl +module "cloud-vpn" { + source = "modules/net-vpn-dynamic/" + project_id = "" + region = "europe-west4" + network = "vpn-network" + name = "cloud-net-to-on-prem" + router_asn = 65001 + tunnels = { + remote-1 = { + bgp_peer = { + address = "169.254.0.2" + asn = 65002 + } + bgp_session_range = "169.254.0.1/30" + ike_version = 2 + peer_ip = module.on-prem.external_address + shared_secret = null + bgp_peer_options = { + advertise_groups = ["ALL_SUBNETS"] + advertise_ip_ranges = { + } + advertise_mode = "DEFAULT" + route_priority = 1000 + } + } + } +} + +module "on-prem" { + source = "modules/on-prem-in-a-box/" + + name = "onprem-instance" + project_id = "" + zone = "europe-west4-b" + network = "" + subnet_self_link = "https://www.googleapis.com/compute/v1/projects//regions/europe-west4/subnetworks/" + vpn_gateway_type = "dynamic" + peer_ip = module.cloud-vpn.address + local_ip_cidr_range = "192.168.192.0/24" + shared_secret = module.cloud-vpn.random_secret + peer_bgp_session_range = "169.254.0.1/30" + local_bgp_session_range = "169.254.0.2/30" + peer_bgp_asn = 65001 + local_bgp_asn = 65002 +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| network | VPC network name. | string | ✓ | | +| project_id | Project id. | string | ✓ | | +| subnet_self_link | VPC subnet self link. | string | ✓ | | +| vpn_config | VPN configuration, type must be one of 'dynamic' or 'static'. | object({...}) | ✓ | | +| zone | Compute zone. | string | ✓ | | +| *coredns_config* | CoreDNS configuration, set to null to use default. | string | | null | +| *dns_domain* | DNS domain used for on-prem host records. | string | | onprem.example.com | +| *local_ip_cidr_range* | IP CIDR range used for the Docker onprem network. | string | | 192.168.192.0/24 | +| *machine_type* | Machine type. | string | | g1-small | +| *name* | On-prem-in-a-box compute instance name. | string | | onprem | +| *network_tags* | Network tags. | list(string) | | ["ssh"] | +| *service_account* | Service account customization. | object({...}) | | ... | +| *vpn_dynamic_config* | BGP configuration for dynamic VPN, ignored if VPN type is 'static'. | object({...}) | | ... | +| *vpn_static_ranges* | Remote CIDR ranges for static VPN, ignored if VPN type is 'dynamic'. | list(string) | | [] | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| dns_ip_address | None | | +| external_address | None | | +| instance_name | None | | +| internal_address | None | | +| toolbox_ip_address | None | | +| vpn_ip_address | None | | +| web_ip_address | None | | + diff --git a/modules/on-prem-in-a-box/assets/Corefile b/modules/on-prem-in-a-box/assets/Corefile new file mode 100644 index 000000000..91672a257 --- /dev/null +++ b/modules/on-prem-in-a-box/assets/Corefile @@ -0,0 +1,11 @@ +${dns_domain} { + root /etc/coredns + hosts onprem.hosts + log + errors +} +. { + forward . 8.8.8.8 + log + errors +} diff --git a/modules/on-prem-in-a-box/assets/dynamic-vpn-gw-cloud-init.yaml b/modules/on-prem-in-a-box/assets/dynamic-vpn-gw-cloud-init.yaml new file mode 100644 index 000000000..05fda88f9 --- /dev/null +++ b/modules/on-prem-in-a-box/assets/dynamic-vpn-gw-cloud-init.yaml @@ -0,0 +1,319 @@ +#cloud-config + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.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. + +package_update: true +package_upgrade: true +package_reboot_if_required: true + +packages: + - apt-transport-https + - ca-certificates + - curl + - gnupg-agent + - software-properties-common + +write_files: + +# Docker daemon configuration +- path: /etc/docker/daemon.json + owner: root:root + permissions: '0644' + content: | + { + "log-driver": "json-file", + "log-opts": { + "max-size": "10m" + } + } + +# Docker compose systemd unit for onprem +- path: /etc/systemd/system/docker-onprem.service + permissions: 0644 + owner: root + content: | + [Install] + WantedBy=multi-user.target + [Unit] + Description=Start Docker Compose onprem infrastructure + After=network-online.target docker.socket + Wants=network-online.target docker.socket + [Service] + ExecStart=/bin/sh -c "cd /var/lib/docker-compose/onprem && /usr/local/bin/docker-compose up" + ExecStop=/bin/sh -c "cd /var/lib/docker-compose/onprem && /usr/local/bin/docker-compose down" + +# Docker compose configuration file for onprem +- path: /var/lib/docker-compose/onprem/docker-compose.yaml + permissions: 0644 + owner: root + content: | + version: "3" + services: + vpn: + image: gcr.io/pso-cft-fabric/strongswan:latest + networks: + onprem: + ipv4_address: ${vpn_ip_address} + ports: + - "500:500/udp" + - "4500:4500/udp" + - "179:179/tcp" + privileged: true + cap_add: + - NET_ADMIN + volumes: + - "/lib/modules:/lib/modules:ro" + - "/etc/localtime:/etc/localtime:ro" + - "/var/lib/docker-compose/onprem/ipsec/ipsec.conf:/etc/ipsec.conf:ro" + - "/var/lib/docker-compose/onprem/ipsec/ipsec.secrets:/etc/ipsec.secrets:ro" + - "/var/lib/docker-compose/onprem/ipsec/vti.conf:/etc/strongswan.d/vti.conf:ro" + environment: + - LAN_NETWORKS=${local_ip_cidr_range} + bird: + image: pierky/bird + network_mode: service:vpn + cap_add: + - NET_ADMIN + - NET_BROADCAST + - NET_RAW + privileged: true + volumes: + - "/var/lib/docker-compose/onprem/bird/bird.conf:/etc/bird/bird.conf:ro" + dns: + image: coredns/coredns + command: "-conf /etc/coredns/Corefile" + depends_on: + - "vpn" + - "bird" + networks: + onprem: + ipv4_address: ${dns_ip_address} + volumes: + - "/var/lib/docker-compose/onprem/coredns:/etc/coredns:ro" + routing_sidecar_dns: + image: alpine + network_mode: service:dns + command: | + /bin/sh -c "\ + ip route del default &&\ + ip route add default via ${vpn_ip_address}" + privileged: true + web: + image: nginx:stable-alpine + depends_on: + - "vpn" + - "bird" + - "dns" + dns: + - ${dns_ip_address} + networks: + onprem: + ipv4_address: ${web_ip_address} + volumes: + - "/var/lib/docker-compose/onprem/nginx:/usr/share/nginx/html:ro" + routing_sidecar_web: + image: alpine + network_mode: service:web + command: | + /bin/sh -c "\ + ip route del default &&\ + ip route add default via ${vpn_ip_address}" + privileged: true + toolbox: + image: gcr.io/pso-cft-fabric/toolbox:latest + networks: + onprem: + ipv4_address: ${toolbox_ip_address} + depends_on: + - "vpn" + - "dns" + - "web" + dns: + - ${dns_ip_address} + routing_sidecar_toolbox: + image: alpine + network_mode: service:toolbox + command: | + /bin/sh -c "\ + ip route del default &&\ + ip route add default via ${vpn_ip_address}" + privileged: true + networks: + onprem: + ipam: + driver: default + config: + - subnet: ${local_ip_cidr_range} + +# IPSEC tunnel secret +- path: /var/lib/docker-compose/onprem/ipsec/ipsec.secrets + owner: root:root + permissions: '0600' + content: | + : PSK "${shared_secret}" + +# IPSEC tunnel configuration +- path: /var/lib/docker-compose/onprem/ipsec/ipsec.conf + owner: root:root + permissions: '0644' + content: | + conn %default + ikelifetime=600m + keylife=180m + rekeymargin=3m + keyingtries=3 + keyexchange=ikev2 + mobike=no + ike=aes256gcm16-sha512-modp2048 + esp=aes256gcm16-sha512-modp8192 + authby=psk + + conn gcp + leftupdown="/var/lib/strongswan/ipsec-vti.sh 0 ${peer_bgp_address}/30 ${local_bgp_address}/30" + left=%any + leftid=%any + leftsubnet=0.0.0.0/0 + leftauth=psk + right=${peer_ip_wildcard} + rightid=${peer_ip} + rightsubnet=0.0.0.0/0 + rightauth=psk + type=tunnel + auto=start + dpdaction=restart + closeaction=restart + mark=%unique + +# Charon configuration +- path: /var/lib/docker-compose/onprem/ipsec/vti.conf + owner: root:root + permissions: '0644' + content: | + charon { + install_routes = no + } + +# Bird bgp routing configuration +- path: /var/lib/docker-compose/onprem/bird/bird.conf + owner: root:root + permissions: '0644' + content: | + router id ${local_bgp_address}; + + # Watch interface up/down events + protocol device { + scan time 10; + } + + # Sync routes to kernel + protocol kernel { + learn; + merge paths on; # For ECMP + export filter { + krt_prefsrc = ${vpn_ip_address}; # Internal IP Address of the strongSwan VM. + accept; # Sync all routes to kernel + }; + import all; # Required due to /32 on GCE VMs for the static route below + } + + # Configure a static route to make sure route exists + protocol static { + # Network connected to eth0 + route ${local_ip_cidr_range} recursive ${local_gw_ip}; + # Private google access + route 199.36.153.4/30 via ${peer_bgp_address}; + # Cloud DNS forwarding zone + route 35.199.192.0/19 via ${peer_bgp_address}; + } + # Prefix lists for routing security + # Allow any possible GCP Subnet + define GCP_VPC_A_PREFIXES = [ 10.0.0.0/8{9,29}, 172.16.0.0/12{12,29}, 192.168.0.0/16{16,29} ]; + define LOCAL_PREFIXES = [ ${local_ip_cidr_range} ]; + + # Filter received prefixes + filter gcp_vpc_a_in + { + if (net ~ GCP_VPC_A_PREFIXES) then accept; + else reject; + } + + # Filter advertised prefixes + filter gcp_vpc_a_out + { + if (net ~ LOCAL_PREFIXES) then accept; + else reject; + } + + template bgp gcp_vpc_a { + keepalive time 20; + hold time 60; + graceful restart aware; # Cloud Router uses GR during maintenance + + import filter gcp_vpc_a_in; + import limit 10 action warn; # restart | block | disable + + export filter gcp_vpc_a_out; + export limit 10 action warn; # restart | block | disable + } + + protocol bgp gcp_vpc_a_tun1 from gcp_vpc_a + { + local ${local_bgp_address} as ${local_bgp_asn}; + neighbor ${peer_bgp_address} as ${peer_bgp_asn}; + } + +# CoreDNS configuration +- path: /var/lib/docker-compose/onprem/coredns/Corefile + owner: root:root + permissions: '0644' + content: | + ${coredns_config} + +# CoreDNS onprem hosts file +- path: /var/lib/docker-compose/onprem/coredns/onprem.hosts + owner: root:root + permissions: '0644' + content: | + ${vpn_ip_address} gw.${dns_domain} + ${dns_ip_address} ns.${dns_domain} + ${web_ip_address} www.${dns_domain} + ${toolbox_ip_address} toolbox.${dns_domain} + +# Minimal nginx index page +- path: /var/lib/docker-compose/onprem/nginx/index.html + owner: root:root + permissions: '0644' + content: | + + + + +

On Prem in a Box

+

${instance_name}

+ + + +runcmd: +- [systemctl, daemon-reload] +- [ sh, -c, 'curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -' ] +- [ sh, -c, 'add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"' ] +- [ sh, -c, 'apt update' ] +- [ sh, -c, 'apt install -y docker-ce docker-ce-cli containerd.io' ] +- [ sh, -c, 'curl -L https://github.com/docker/compose/releases/download/$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep "tag_name" | cut -d \" -f4)/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose' ] +- [ sh, -c, 'chmod 755 /usr/local/bin/docker-compose' ] +- [systemctl, enable, docker.service] +- [systemctl, start, docker.service] +- [systemctl, enable, docker-onprem.service] +- [systemctl, start, docker-onprem.service] diff --git a/modules/on-prem-in-a-box/assets/static-vpn-gw-cloud-init.yaml b/modules/on-prem-in-a-box/assets/static-vpn-gw-cloud-init.yaml new file mode 100644 index 000000000..67f7531b9 --- /dev/null +++ b/modules/on-prem-in-a-box/assets/static-vpn-gw-cloud-init.yaml @@ -0,0 +1,225 @@ +#cloud-config + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.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. + +package_update: true +package_upgrade: true +package_reboot_if_required: true + +packages: + - apt-transport-https + - ca-certificates + - curl + - gnupg-agent + - software-properties-common + +write_files: + +# Docker daemon configuration +- path: /etc/docker/daemon.json + owner: root:root + permissions: '0644' + content: | + { + "log-driver": "json-file", + "log-opts": { + "max-size": "10m" + } + } + +# Docker compose systemd unit for onprem +- path: /etc/systemd/system/docker-onprem.service + permissions: 0644 + owner: root + content: | + [Install] + WantedBy=multi-user.target + [Unit] + Description=Start Docker Compose onprem infrastructure + After=network-online.target docker.socket + Wants=network-online.target docker.socket + [Service] + ExecStart=/bin/sh -c "cd /var/lib/docker-compose/onprem && /usr/local/bin/docker-compose up" + ExecStop=/bin/sh -c "cd /var/lib/docker-compose/onprem && /usr/local/bin/docker-compose down" + +# Docker compose configuration file for onprem +- path: /var/lib/docker-compose/onprem/docker-compose.yaml + permissions: 0644 + owner: root + content: | + version: "3" + services: + vpn: + image: gcr.io/pso-cft-fabric/strongswan:latest + networks: + onprem: + ipv4_address: ${vpn_ip_address} + ports: + - "500:500/udp" + - "4500:4500/udp" + privileged: true + cap_add: + - NET_ADMIN + volumes: + - "/lib/modules:/lib/modules:ro" + - "/etc/localtime:/etc/localtime:ro" + - "/var/lib/docker-compose/onprem/ipsec/ipsec.conf:/etc/ipsec.conf:ro" + - "/var/lib/docker-compose/onprem/ipsec/ipsec.secrets:/etc/ipsec.secrets:ro" + environment: + - LAN_NETWORKS=${local_ip_cidr_range} + dns: + image: coredns/coredns + command: "-conf /etc/coredns/Corefile" + depends_on: + - "vpn" + networks: + onprem: + ipv4_address: ${dns_ip_address} + volumes: + - "/var/lib/docker-compose/onprem/coredns:/etc/coredns:ro" + routing_sidecar_dns: + image: alpine + network_mode: service:dns + command: | + /bin/sh -c "\ + ip route del default &&\ + ip route add default via ${vpn_ip_address}" + privileged: true + web: + image: nginx:stable-alpine + depends_on: + - "vpn" + - "dns" + dns: + - ${dns_ip_address} + networks: + onprem: + ipv4_address: ${web_ip_address} + volumes: + - "/var/lib/docker-compose/onprem/nginx:/usr/share/nginx/html:ro" + routing_sidecar_web: + image: alpine + network_mode: service:web + command: | + /bin/sh -c "\ + ip route del default &&\ + ip route add default via ${vpn_ip_address}" + privileged: true + toolbox: + image: gcr.io/pso-cft-fabric/toolbox:latest + networks: + onprem: + ipv4_address: ${toolbox_ip_address} + depends_on: + - "vpn" + - "dns" + - "web" + dns: + - ${dns_ip_address} + routing_sidecar_toolbox: + image: alpine + network_mode: service:toolbox + command: | + /bin/sh -c "\ + ip route del default &&\ + ip route add default via ${vpn_ip_address}" + privileged: true + networks: + onprem: + ipam: + driver: default + config: + - subnet: ${local_ip_cidr_range} + +# IPSEC tunnel secret +- path: /var/lib/docker-compose/onprem/ipsec/ipsec.secrets + owner: root:root + permissions: '0600' + content: | + : PSK "${shared_secret}" + +# IPSEC tunnel configuration +- path: /var/lib/docker-compose/onprem/ipsec/ipsec.conf + owner: root:root + permissions: '0644' + content: | + conn %default + ikelifetime=600m + keylife=180m + rekeymargin=3m + keyingtries=3 + keyexchange=ikev2 + mobike=no + ike=aes256gcm16-sha512-modp2048 + esp=aes256gcm16-sha512-modp8192 + authby=psk + + conn gcp + left=%any + leftid=%any + leftsubnet=${local_ip_cidr_range} + leftauth=psk + right=${peer_ip_wildcard} + rightid=${peer_ip} + rightsubnet=199.36.153.4/30,35.199.192.0/19,${remote_ip_cidr_ranges} + rightauth=psk + type=tunnel + auto=start + dpdaction=restart + closeaction=restart + +# CoreDNS configuration +- path: /var/lib/docker-compose/onprem/coredns/Corefile + owner: root:root + permissions: '0644' + content: | + ${coredns_config} + +# CoreDNS onprem hosts file +- path: /var/lib/docker-compose/onprem/coredns/onprem.hosts + owner: root:root + permissions: '0644' + content: | + ${vpn_ip_address} gw.${dns_domain} + ${dns_ip_address} ns.${dns_domain} + ${web_ip_address} www.${dns_domain} + ${toolbox_ip_address} toolbox.${dns_domain} + +# Minimal nginx index page +- path: /var/lib/docker-compose/onprem/nginx/index.html + owner: root:root + permissions: '0644' + content: | + + + + +

On Prem in a Box

+

${instance_name}

+ + + +runcmd: +- [systemctl, daemon-reload] +- [ sh, -c, 'curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -' ] +- [ sh, -c, 'add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"' ] +- [ sh, -c, 'apt update' ] +- [ sh, -c, 'apt install -y docker-ce docker-ce-cli containerd.io' ] +- [ sh, -c, 'curl -L https://github.com/docker/compose/releases/download/$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep "tag_name" | cut -d \" -f4)/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose' ] +- [ sh, -c, 'chmod 755 /usr/local/bin/docker-compose' ] +- [systemctl, enable, docker.service] +- [systemctl, start, docker.service] +- [systemctl, enable, docker-onprem.service] +- [systemctl, start, docker-onprem.service] diff --git a/modules/on-prem-in-a-box/main.tf b/modules/on-prem-in-a-box/main.tf new file mode 100644 index 000000000..1ddfc34df --- /dev/null +++ b/modules/on-prem-in-a-box/main.tf @@ -0,0 +1,130 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.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. + */ + +locals { + corefile = ( + var.coredns_config == null || var.coredns_config == "" + ? data.template_file.corefile.rendered + : var.coredns_config + ) +} + +resource "google_compute_address" "static" { + project = var.project_id + name = var.name + region = substr(var.zone, 0, length(var.zone) - 2) + address_type = "EXTERNAL" +} + +resource "google_compute_instance" "on_prem_in_a_box" { + project = var.project_id + name = var.name + machine_type = var.machine_type + zone = var.zone + tags = concat(var.network_tags, ["onprem"]) + + boot_disk { + initialize_params { + image = "ubuntu-os-cloud/ubuntu-1804-lts" + } + } + + network_interface { + subnetwork = var.subnet_self_link + access_config { + nat_ip = google_compute_address.static.address + } + } + + metadata = { + user-data = data.template_file.vpn-gw.rendered + } + + service_account { + email = var.service_account.email + scopes = var.service_account.scopes + } + +} + +data "template_file" "corefile" { + template = file("${path.module}/assets/Corefile") + vars = { + dns_domain = var.dns_domain + } +} + +data "template_file" "vpn-gw" { + template = file(format( + "%s/assets/%s-vpn-gw-cloud-init.yaml", path.module, var.vpn_config.type + )) + + vars = { + coredns_config = indent(4, local.corefile) + dns_domain = var.dns_domain + instance_name = var.name + local_ip_cidr_range = var.local_ip_cidr_range + local_gw_ip = cidrhost(var.local_ip_cidr_range, 1) + vpn_ip_address = cidrhost(var.local_ip_cidr_range, 2) + dns_ip_address = cidrhost(var.local_ip_cidr_range, 3) + web_ip_address = cidrhost(var.local_ip_cidr_range, 4) + toolbox_ip_address = cidrhost(var.local_ip_cidr_range, 5) + # vpn config + peer_ip = var.vpn_config.peer_ip + peer_ip_wildcard = "%${var.vpn_config.peer_ip}" + shared_secret = var.vpn_config.shared_secret + # vpn dynamic config + local_bgp_asn = var.vpn_dynamic_config.local_bgp_asn + local_bgp_address = var.vpn_dynamic_config.local_bgp_address + peer_bgp_asn = var.vpn_dynamic_config.peer_bgp_asn + peer_bgp_address = var.vpn_dynamic_config.peer_bgp_address + # vpn static ranges + vpn_static_ranges = join(",", var.vpn_static_ranges) + } +} + +# TODO: use a narrower firewall rule and tie it to the service account + +resource "google_compute_firewall" "allow-vpn" { + name = "onprem-in-a-box-allow-vpn" + description = "Allow VPN traffic to the onprem instance" + network = var.network + project = var.project_id + source_ranges = [format("%s/32", var.vpn_config.peer_ip)] + target_tags = ["onprem"] + allow { + protocol = "tcp" + } + allow { + protocol = "udp" + } + allow { + protocol = "icmp" + } +} + +resource "google_compute_firewall" "allow-iap" { + name = "onprem-in-a-box-allow-iap" + description = "Allow SSH traffic to the onprem instance from IAP" + network = var.network + project = var.project_id + source_ranges = ["35.235.240.0/20"] + target_tags = ["onprem"] + allow { + protocol = "tcp" + ports = ["22"] + } +} diff --git a/modules/on-prem-in-a-box/outputs.tf b/modules/on-prem-in-a-box/outputs.tf new file mode 100644 index 000000000..4404de163 --- /dev/null +++ b/modules/on-prem-in-a-box/outputs.tf @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "external_address" { + value = google_compute_instance.on_prem_in_a_box.network_interface.0.access_config.0.nat_ip +} + +output "internal_address" { + value = google_compute_instance.on_prem_in_a_box.network_interface.0.network_ip +} + +output "instance_name" { + value = google_compute_instance.on_prem_in_a_box.name +} + +output "vpn_ip_address" { + value = cidrhost(var.local_ip_cidr_range, 2) +} + +output "dns_ip_address" { + value = cidrhost(var.local_ip_cidr_range, 3) +} + +output "web_ip_address" { + value = cidrhost(var.local_ip_cidr_range, 4) +} + +output "toolbox_ip_address" { + value = cidrhost(var.local_ip_cidr_range, 5) +} diff --git a/modules/on-prem-in-a-box/variables.tf b/modules/on-prem-in-a-box/variables.tf new file mode 100644 index 000000000..b636f2445 --- /dev/null +++ b/modules/on-prem-in-a-box/variables.tf @@ -0,0 +1,118 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "coredns_config" { + description = "CoreDNS configuration, set to null to use default." + type = string + default = null +} + +variable "dns_domain" { + description = "DNS domain used for on-prem host records." + type = string + default = "onprem.example.com" +} + +variable "local_ip_cidr_range" { + description = "IP CIDR range used for the Docker onprem network." + type = string + default = "192.168.192.0/24" +} + +variable "machine_type" { + description = "Machine type." + type = string + default = "g1-small" +} + +variable "name" { + description = "On-prem-in-a-box compute instance name." + type = string + default = "onprem" +} + +variable "network" { + description = "VPC network name." + type = string +} + +variable "network_tags" { + description = "Network tags." + type = list(string) + default = ["ssh"] +} + +variable "project_id" { + description = "Project id." + type = string +} + +variable "service_account" { + description = "Service account customization." + type = object({ + email = string + scopes = list(string) + }) + default = { + email = null + scopes = [ + "https://www.googleapis.com/auth/devstorage.read_only", + "https://www.googleapis.com/auth/logging.write", + "https://www.googleapis.com/auth/monitoring.write" + ] + } +} + +variable "subnet_self_link" { + description = "VPC subnet self link." + type = string +} + +variable "vpn_config" { + description = "VPN configuration, type must be one of 'dynamic' or 'static'." + type = object({ + peer_ip = string + shared_secret = string + type = string + }) +} + +variable "vpn_dynamic_config" { + description = "BGP configuration for dynamic VPN, ignored if VPN type is 'static'." + type = object({ + local_bgp_asn = number + local_bgp_address = string + peer_bgp_asn = number + peer_bgp_address = string + }) + default = { + local_bgp_asn = 65002 + local_bgp_address = "169.254.0.2" + peer_bgp_asn = 65001 + peer_bgp_address = "169.254.0.1" + } +} + +variable "vpn_static_ranges" { + description = "Remote CIDR ranges for static VPN, ignored if VPN type is 'dynamic'." + type = list(string) + default = [] +} + +variable "zone" { + description = "Compute zone." + type = string +} diff --git a/modules/project/README.md b/modules/project/README.md new file mode 100644 index 000000000..2d79bd688 --- /dev/null +++ b/modules/project/README.md @@ -0,0 +1,59 @@ +# Project Module + +## Example + +```hcl +module "project" { + source = "./modules/project" + parent = var.folder.id + billing_account = var.billing_account_id + prefix = "foo" + name = "project-example" + oslogin = true + oslogin_admins = var.admins + services = concat(var.project_services, [ + "cloudkms.googleapis.com", "accesscontextmanager.googleapis.com" + ]) + iam_roles = ["roles/container.hostServiceAgentUser"] + iam_members = { "roles/container.hostServiceAgentUser" = [ + "serviceAccount:${var.gke_service_account}" + ] } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| name | Project name and id suffix. | string | ✓ | | +| parent | The resource name of the parent Folder or Organization. Must be of the form folders/folder_id or organizations/org_id. | string | ✓ | | +| *auto_create_network* | Whether to create the default network for the project | bool | | false | +| *billing_account* | Billing account id. | string | | | +| *custom_roles* | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| *iam_additive_members* | Map of member lists used to set non authoritative bindings, keyed by role. | map(list(string)) | | {} | +| *iam_additive_roles* | List of roles used to set non authoritative bindings. | list(string) | | [] | +| *iam_members* | Map of member lists used to set authoritative bindings, keyed by role. | map(list(string)) | | {} | +| *iam_roles* | List of roles used to set authoritative bindings. | list(string) | | [] | +| *labels* | Resource labels. | map(string) | | {} | +| *lien_reason* | If non-empty, creates a project lien with this description. | string | | | +| *oslogin* | Enable OS Login. | bool | | false | +| *oslogin_admins* | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | +| *oslogin_users* | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | +| *prefix* | Prefix used to generate project id and name. | string | | null | +| *services* | Service APIs to enable. | list(string) | | [] | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| cloudsvc_service_account | Cloud services service account (depends on services). | | +| custom_roles | Ids of the created custom roles. | | +| gce_service_account | Default GCE service account (depends on services). | | +| gcr_service_account | Default GCR service account (depends on services). | | +| gke_service_account | Default GKE service account (depends on services). | | +| iam_project_id | Project id (depends on services and IAM bindings). | | +| name | Name (depends on services). | | +| number | Project number (depends on services). | | +| project_id | Project id (depends on services). | | + diff --git a/modules/project/main.tf b/modules/project/main.tf new file mode 100644 index 000000000..e6d8bab67 --- /dev/null +++ b/modules/project/main.tf @@ -0,0 +1,126 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + cloudsvc_service_account = "${google_project.project.number}@cloudservices.gserviceaccount.com" + gce_service_account = "${google_project.project.number}-compute@developer.gserviceaccount.com" + gcr_service_account = "service-${google_project.project.number}@containerregistry.iam.gserviceaccount.com" + gke_service_account = "service-${google_project.project.number}@container-engine-robot.iam.gserviceaccount.com" + iam_additive_pairs = flatten([ + for role in var.iam_additive_roles : [ + for member in lookup(var.iam_additive_members, role, []) : + { role = role, member = member } + ] + ]) + iam_additive = { + for pair in local.iam_additive_pairs : + "${pair.role}-${pair.member}" => pair + } + parent_type = split("/", var.parent)[0] + parent_id = split("/", var.parent)[1] + prefix = var.prefix == null ? "" : "${var.prefix}-" +} + +resource "google_project" "project" { + org_id = local.parent_type == "organizations" ? local.parent_id : null + folder_id = local.parent_type == "folders" ? local.parent_id : null + project_id = "${local.prefix}${var.name}" + name = "${local.prefix}${var.name}" + billing_account = var.billing_account + auto_create_network = var.auto_create_network + labels = var.labels +} + +resource "google_project_iam_custom_role" "roles" { + for_each = var.custom_roles + project = google_project.project.project_id + role_id = each.key + title = "Custom role ${each.key}" + description = "Terraform-managed" + permissions = each.value +} + +resource "google_compute_project_metadata_item" "oslogin_meta" { + count = var.oslogin ? 1 : 0 + project = google_project.project.project_id + key = "enable-oslogin" + value = "TRUE" + # depend on services or it will fail on destroy + depends_on = [google_project_service.project_services] +} + +resource "google_resource_manager_lien" "lien" { + count = var.lien_reason != "" ? 1 : 0 + parent = "projects/${google_project.project.number}" + restrictions = ["resourcemanager.projects.delete"] + origin = "created-by-terraform" + reason = var.lien_reason +} + +resource "google_project_service" "project_services" { + for_each = toset(var.services) + project = google_project.project.project_id + service = each.value + disable_on_destroy = true + disable_dependent_services = true +} + +# IAM notes: +# - external users need to have accepted the invitation email to join +# - oslogin roles also require role to list instances +# - additive (non-authoritative) roles might fail due to dynamic values + +resource "google_project_iam_binding" "authoritative" { + for_each = toset(var.iam_roles) + project = google_project.project.project_id + role = each.value + members = lookup(var.iam_members, each.value, []) +} + +resource "google_project_iam_member" "additive" { + for_each = length(var.iam_additive_roles) > 0 ? local.iam_additive : {} + project = google_project.project.project_id + role = each.value.role + member = each.value.member +} + +resource "google_project_iam_member" "oslogin_iam_serviceaccountuser" { + for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([]) + project = google_project.project.project_id + role = "roles/iam.serviceAccountUser" + member = each.value +} + +resource "google_project_iam_member" "oslogin_compute_viewer" { + for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([]) + project = google_project.project.project_id + role = "roles/compute.viewer" + member = each.value +} + +resource "google_project_iam_member" "oslogin_admins" { + for_each = var.oslogin ? toset(var.oslogin_admins) : toset([]) + project = google_project.project.project_id + role = "roles/compute.osAdminLogin" + member = each.value +} + +resource "google_project_iam_member" "oslogin_users" { + for_each = var.oslogin ? toset(var.oslogin_users) : toset([]) + project = google_project.project.project_id + role = "roles/compute.osLogin" + member = each.value +} diff --git a/modules/project/outputs.tf b/modules/project/outputs.tf new file mode 100644 index 000000000..7cb702174 --- /dev/null +++ b/modules/project/outputs.tf @@ -0,0 +1,74 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "project_id" { + description = "Project id (depends on services)." + value = google_project.project.project_id + depends_on = [ + google_project_service.project_services + ] +} + +output "iam_project_id" { + description = "Project id (depends on services and IAM bindings)." + value = google_project.project.project_id + depends_on = [ + google_project_service.project_services, + google_project_iam_binding.authoritative, + google_project_iam_member.non_authoritative + ] +} + +output "name" { + description = "Name (depends on services)." + value = google_project.project.name + depends_on = [google_project_service.project_services] +} + +output "number" { + description = "Project number (depends on services)." + value = google_project.project.number + depends_on = [google_project_service.project_services] +} + +output "cloudsvc_service_account" { + description = "Cloud services service account (depends on services)." + value = "${local.cloudsvc_service_account}" + depends_on = [google_project_service.project_services] +} + +output "gce_service_account" { + description = "Default GCE service account (depends on services)." + value = local.gce_service_account + depends_on = [google_project_service.project_services] +} + +output "gcr_service_account" { + description = "Default GCR service account (depends on services)." + value = local.gcr_service_account + depends_on = [google_project_service.project_services] +} + +output "gke_service_account" { + description = "Default GKE service account (depends on services)." + value = local.gke_service_account + depends_on = [google_project_service.project_services] +} + +output "custom_roles" { + description = "Ids of the created custom roles." + value = [for role in google_project_iam_custom_role.roles : role.role_id] +} diff --git a/modules/project/variables.tf b/modules/project/variables.tf new file mode 100644 index 000000000..b9ecc8645 --- /dev/null +++ b/modules/project/variables.tf @@ -0,0 +1,109 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "auto_create_network" { + description = "Whether to create the default network for the project" + type = bool + default = false +} + +variable "billing_account" { + description = "Billing account id." + type = string + default = "" +} + +variable "custom_roles" { + description = "Map of role name => list of permissions to create in this project." + type = map(list(string)) + default = {} +} + +variable "iam_members" { + description = "Map of member lists used to set authoritative bindings, keyed by role." + type = map(list(string)) + default = {} +} + +variable "iam_roles" { + description = "List of roles used to set authoritative bindings." + type = list(string) + default = [] +} + +variable "iam_additive_members" { + description = "Map of member lists used to set non authoritative bindings, keyed by role." + type = map(list(string)) + default = {} +} + +variable "iam_additive_roles" { + description = "List of roles used to set non authoritative bindings." + type = list(string) + default = [] +} + +variable "labels" { + description = "Resource labels." + type = map(string) + default = {} +} + +variable "lien_reason" { + description = "If non-empty, creates a project lien with this description." + type = string + default = "" +} + +variable "name" { + description = "Project name and id suffix." + type = string +} + +variable "oslogin" { + description = "Enable OS Login." + type = bool + default = false +} + +variable "oslogin_admins" { + description = "List of IAM-style identities that will be granted roles necessary for OS Login administrators." + type = list(string) + default = [] +} + +variable "oslogin_users" { + description = "List of IAM-style identities that will be granted roles necessary for OS Login users." + type = list(string) + default = [] +} + +variable "parent" { + description = "The resource name of the parent Folder or Organization. Must be of the form folders/folder_id or organizations/org_id." + type = string +} + +variable "prefix" { + description = "Prefix used to generate project id and name." + type = string + default = null +} + +variable "services" { + description = "Service APIs to enable." + type = list(string) + default = [] +} diff --git a/modules/project/versions.tf b/modules/project/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/project/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = ">= 0.12.6" +} diff --git a/tests/__init__.py b/tests/__init__.py index 35dff2e86..6913f02e3 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,14 +1,13 @@ -# Copyright 2019 Google LLC -# +# Copyright 2020 Google LLC +# # Licensed under the Apache License, Version 2.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 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - diff --git a/tests/conftest.py b/tests/conftest.py index 3dea14be8..b42458c6d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,10 @@ -# Copyright 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.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 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -12,25 +12,47 @@ # See the License for the specific language governing permissions and # limitations under the License. -"Shared fixtures." +"Shared fixtures" import os - import pytest import tftest -_BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASEDIR = os.path.dirname(os.path.dirname(__file__)) @pytest.fixture(scope='session') -def plan(): +def plan_runner(): + "Returns a function to run Terraform plan on a fixture." - def run_plan(testdir): - tfdir = testdir.replace('_', '-') - tf = tftest.TerraformTest(tfdir, _BASEDIR, + def run_plan(fixture_path, is_module=True, **tf_vars): + "Runs Terraform plan and returns parsed output" + tf = tftest.TerraformTest(fixture_path, BASEDIR, os.environ.get('TERRAFORM', 'terraform')) - tf.setup(extra_files=['tests/{}/terraform.tfvars'.format(testdir)]) - return tf.plan(output=True) + tf.setup() + plan = tf.plan(output=True, tf_vars=tf_vars) + root_module = plan.planned_values['root_module']['child_modules'][0] + if is_module: + return (plan, root_module['resources']) + modules = dict((mod['address'], mod['resources']) + for mod in root_module['child_modules']) + return (plan, modules) return run_plan + + +@pytest.fixture(scope='session') +def apply_runner(): + "Returns a function to run Terraform apply on a fixture." + + def run_apply(fixture_path, **tf_vars): + "Runs Terraform apply and returns parsed output" + tf = tftest.TerraformTest(fixture_path, BASEDIR, + os.environ.get('TERRAFORM', 'terraform')) + tf.setup() + apply = tf.apply(tf_vars=tf_vars) + output = tf.output(json_format=True) + return (apply, output) + + return run_apply diff --git a/tests/foundations/__init__.py b/tests/foundations/__init__.py index 35dff2e86..6913f02e3 100644 --- a/tests/foundations/__init__.py +++ b/tests/foundations/__init__.py @@ -1,14 +1,13 @@ -# Copyright 2019 Google LLC -# +# Copyright 2020 Google LLC +# # Licensed under the Apache License, Version 2.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 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - diff --git a/tests/foundations/business_units/terraform.tfvars b/tests/foundations/business_units/terraform.tfvars deleted file mode 100644 index 199cdc94d..000000000 --- a/tests/foundations/business_units/terraform.tfvars +++ /dev/null @@ -1,9 +0,0 @@ -billing_account_id = "012345-ABCDEF-012345" -business_unit_1_name = "infra" -business_unit_2_name = "analytics" -business_unit_3_name = "data" -environments = ["dev", "test"] -generate_service_account_keys = true -organization_id = "012345678919" -prefix = "fabric-org-env-3" -root_node = "folders/0123456789" diff --git a/tests/foundations/business_units/test_folders.py b/tests/foundations/business_units/test_folders.py deleted file mode 100644 index 2064922b3..000000000 --- a/tests/foundations/business_units/test_folders.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -"Test shared and business-units folders" - - -import pytest - - -def test_shared_folder(plan): - "Shared folder resource attributes must match variables." - root_node = plan.variables['root_node'] - resource = plan.modules['module.shared-folder'].resources['google_folder.folders[0]'] - assert resource['values']['parent'] == root_node - assert resource['values']['display_name'] == 'shared' - - -def test_business_unit_folders(plan): - "Business Unit folder resource attributes must match variables." - address_tpl = ( - 'module.business-unit-%s-folders.module.business-unit-folder' - '.google_folder.folders[0]' - ) - count = range(1, 4) - business_unit_names = [ - plan.variables['business_unit_%s_name' % i] for i in count] - root_node = plan.variables['root_node'] - for address in [address_tpl % i for i in count]: - resource = plan.resource_changes[address] - assert resource['change']['after']['parent'] == root_node - assert resource['change']['after']['display_name'] in business_unit_names diff --git a/tests/foundations/business_units/test_outputs.py b/tests/foundations/business_units/test_outputs.py deleted file mode 100644 index c485b2e8c..000000000 --- a/tests/foundations/business_units/test_outputs.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -"Test root module outputs." - - -def test_project_ids(plan): - "Project ids should use prefix and match expected values." - prefix = plan.variables['prefix'] - assert plan.outputs['audit_logs_project'] == '%s-audit' % prefix - assert plan.outputs['shared_resources_project'] == '%s-shared' % prefix - assert plan.outputs['terraform_project'] == '%s-terraform' % prefix - - -def test_bucket_names(plan): - "GCS bucket names should use prefix and location and match expected values." - location = plan.variables['gcs_location'].lower() - prefix = plan.variables['prefix'] - bootstrap_bucket = plan.outputs['bootstrap_tf_gcs_bucket'] - assert bootstrap_bucket.startswith(prefix) - assert bootstrap_bucket.endswith('tf-bootstrap') - assert '-%s-' % location in bootstrap_bucket - - -def test_environment_buckets(plan): - "One GCS bucket should be created for each environment." - buckets = plan.outputs['environment_tf_gcs_buckets'] - for environment in plan.variables['environments']: - assert environment in buckets - assert buckets[environment].endswith(environment) - - -def test_bq_dataset(plan): - "Bigquery dataset name should be based on root node type and id." - node_type, node_id = plan.variables['root_node'].split('/') - assert plan.outputs['audit_logs_bq_dataset'] == 'logs_audit_%s_%s' % ( - node_type[:-1], node_id) diff --git a/tests/foundations/business_units/test_projects.py b/tests/foundations/business_units/test_projects.py deleted file mode 100644 index 23194afa5..000000000 --- a/tests/foundations/business_units/test_projects.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -"Test project creation in root module." - - -import pytest - - -@pytest.fixture(scope='module') -def project_modules(plan): - names = ['module.project-%s' % - name for name in ('audit', 'shared-resources', 'tf')] - return dict((name, plan.modules[name]) for name in names) - - -def test_project_resource(plan, project_modules): - "Project resource attributes must match variables." - names = ('shared', 'terraform', 'audit') - prefix = plan.variables['prefix'] - billing_account = plan.variables['billing_account_id'] - project_names = ['%s-%s' % (prefix, name) for name in names] - for mod in project_modules.values(): - resource = mod.resources['google_project.project'] - assert resource['values']['billing_account'] == billing_account - assert resource['values']['name'] in project_names - - -def test_project_services(plan, project_modules): - "Project service resource must enable APIs specified in the variable." - num_services = len(plan.variables['project_services']) - for mod in project_modules.values(): - project_services = [r for r in mod.resources if r.startswith( - 'google_project_service.project_services')] - assert len(project_services) >= num_services diff --git a/tests/foundations/business_units/test_service_accounts.py b/tests/foundations/business_units/test_service_accounts.py deleted file mode 100644 index 65640f1a2..000000000 --- a/tests/foundations/business_units/test_service_accounts.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -"Test service account creation in root module." - - -import pytest - - -@pytest.fixture(scope='module') -def mod(plan): - return plan.modules['module.service-accounts-tf-environments'] - - -def test_accounts(plan, mod): - "One service account per environment should be created." - environments = plan.variables['environments'] - prefix = plan.variables['prefix'] - resources = [ - v for k, v in mod.resources.items() if 'google_service_account.' in k] - assert len(resources) == len(environments) - assert sorted([res['values']['account_id'] for res in resources]) == sorted([ - '%s-%s' % (prefix, env) for env in environments]) diff --git a/tests/foundations/environments/__init__.py b/tests/foundations/environments/__init__.py index 35dff2e86..6913f02e3 100644 --- a/tests/foundations/environments/__init__.py +++ b/tests/foundations/environments/__init__.py @@ -1,14 +1,13 @@ -# Copyright 2019 Google LLC -# +# Copyright 2020 Google LLC +# # Licensed under the Apache License, Version 2.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 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - diff --git a/tests/foundations/environments/conftest.py b/tests/foundations/environments/conftest.py deleted file mode 100644 index cb648e15a..000000000 --- a/tests/foundations/environments/conftest.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -"Plan fixture." - -import os - -import pytest - - -_TFDIR = os.path.sep.join(os.path.abspath(__file__).split(os.path.sep)[-3:-1]) - - -@pytest.fixture(scope='package') -def plan(plan): - return plan(_TFDIR) diff --git a/tests/foundations/environments/fixture/main.tf b/tests/foundations/environments/fixture/main.tf new file mode 100644 index 000000000..ba5eef11d --- /dev/null +++ b/tests/foundations/environments/fixture/main.tf @@ -0,0 +1,30 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "test" { + source = "../../../../foundations/environments" + billing_account_id = var.billing_account_id + environments = var.environments + iam_assets_editors = var.iam_assets_editors + iam_assets_owners = var.iam_assets_owners + iam_audit_viewers = var.iam_audit_viewers + iam_sharedsvc_owners = var.iam_sharedsvc_owners + iam_terraform_owners = var.iam_terraform_owners + iam_xpn_config = var.iam_xpn_config + organization_id = var.organization_id + prefix = var.prefix + root_node = var.root_node +} diff --git a/tests/foundations/environments/fixture/variables.tf b/tests/foundations/environments/fixture/variables.tf new file mode 100644 index 000000000..1516e4cd3 --- /dev/null +++ b/tests/foundations/environments/fixture/variables.tf @@ -0,0 +1,76 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +variable "billing_account_id" { + type = string + default = "1234-5678-9012" +} + +variable "environments" { + type = list(string) + default = ["test", "prod"] +} + +variable "iam_assets_editors" { + type = list(string) + default = ["user:assets-ed-1@example.org", "user:assets-ed-2@example.org"] +} + +variable "iam_assets_owners" { + type = list(string) + default = ["user:assets-own-1@example.org", "user:assets-own-2@example.org"] +} + +variable "iam_audit_viewers" { + type = list(string) + default = ["user:audit-1@example.org", "user:audit2@example.org"] +} + +variable "iam_sharedsvc_owners" { + type = list(string) + default = ["user:shared-1@example.org", "user:shared-2@example.org"] +} + +variable "iam_terraform_owners" { + type = list(string) + default = ["user:tf-1@example.org", "user:tf-2@example.org"] +} + +variable "iam_xpn_config" { + type = object({ + grant = bool + target_org = bool + }) + default = { + grant = true + target_org = false + } +} + +variable "organization_id" { + type = string + default = "" +} + +variable "prefix" { + description = "Prefix used for resources that need unique names." + type = string + default = "test" +} + +variable "root_node" { + description = "Root node for the new hierarchy, either 'organizations/org_id' or 'folders/folder_id'." + type = string + default = "folders/1234567890" +} diff --git a/tests/foundations/environments/terraform.tfvars b/tests/foundations/environments/terraform.tfvars deleted file mode 100644 index acc2a16bf..000000000 --- a/tests/foundations/environments/terraform.tfvars +++ /dev/null @@ -1,7 +0,0 @@ -billing_account_id = "012345-ABCDEF-012345" -environments = ["dev", "test"] -generate_service_account_keys = true -grant_xpn_roles = true -organization_id = "012345678919" -prefix = "fabric-org-env-3" -root_node = "folders/0123456789" diff --git a/tests/foundations/environments/test_outputs.py b/tests/foundations/environments/test_outputs.py deleted file mode 100644 index 64f3ca391..000000000 --- a/tests/foundations/environments/test_outputs.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -"Test root module outputs." - - -def test_project_ids(plan): - "Project ids should use prefix and match expected values." - prefix = plan.variables['prefix'] - assert plan.outputs['audit_logs_project'] == prefix + '-audit' - assert plan.outputs['shared_resources_project'] == prefix + '-shared' - assert plan.outputs['terraform_project'] == prefix + '-terraform' - - -def test_bucket_names(plan): - "GCS bucket names should use prefix and location and match expected values." - location = plan.variables['gcs_location'].lower() - prefix = plan.variables['prefix'] - bootstrap_bucket = plan.outputs['bootstrap_tf_gcs_bucket'] - assert bootstrap_bucket.startswith(prefix) - assert bootstrap_bucket.endswith('tf-bootstrap') - assert '-%s-' % location in bootstrap_bucket - - -def test_environment_buckets(plan): - "One GCS bucket should be created for each environment." - buckets = plan.outputs['environment_tf_gcs_buckets'] - for environment in plan.variables['environments']: - assert environment in buckets - assert buckets[environment].endswith(environment) - - -def test_bq_dataset(plan): - "Bigquery dataset should be named after the first environment." - assert plan.outputs['audit_logs_bq_dataset'].endswith( - plan.variables['environments'][0]) diff --git a/tests/foundations/environments/test_plan.py b/tests/foundations/environments/test_plan.py new file mode 100644 index 000000000..96fd124e5 --- /dev/null +++ b/tests/foundations/environments/test_plan.py @@ -0,0 +1,53 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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_folder_roles(plan_runner): + "Test folder roles." + _, modules = plan_runner(FIXTURES_DIR, is_module=False) + resources = modules['module.test.module.environment-folders'] + folders = [r for r in resources if r['type'] == 'google_folder'] + assert len(folders) == 2 + assert set(r['values']['display_name'] + for r in folders) == set(['prod', 'test']) + bindings = [r['index'].split('-') + for r in resources if r['type'] == 'google_folder_iam_binding'] + assert len(bindings) == 10 + assert set(b[0] for b in bindings) == set(['prod', 'test']) + assert len(set(b[1] for b in bindings)) == 5 + + +def test_org_roles(plan_runner): + "Test folder roles." + vars = { + 'organization_id': 'organizations/123', + 'iam_xpn_config': '{grant = true, target_org = true}' + } + _, modules = plan_runner(FIXTURES_DIR, is_module=False, **vars) + resources = modules['module.test.module.environment-folders'] + folder_bindings = [r['index'].split('-') + for r in resources if r['type'] == 'google_folder_iam_binding'] + assert len(folder_bindings) == 8 + resources = modules['module.test.module.tf-service-accounts'] + org_bindings = [r['index'].split('-') + for r in resources if r['type'] == 'google_organization_iam_member'] + assert len(org_bindings) == 4 + assert set(b[0] for b in org_bindings) == set(['prod', 'test']) diff --git a/tests/foundations/environments/test_projects.py b/tests/foundations/environments/test_projects.py deleted file mode 100644 index 26d30639a..000000000 --- a/tests/foundations/environments/test_projects.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -"Test project creation in root module." - - -import pytest - - -@pytest.fixture(scope='module') -def project_modules(plan): - names = ['module.project-%s' % - name for name in ('audit', 'shared-resources', 'tf')] - return dict((name, plan.modules[name]) for name in names) - - -def test_project_resource(plan, project_modules): - "Project resource attributes must match variables." - root_node = plan.variables['root_node'].split('/')[1] - billing_account = plan.variables['billing_account_id'] - for name, mod in project_modules.items(): - resource = mod.resources['google_project.project'] - assert resource['values']['folder_id'] == root_node - assert resource['values']['billing_account'] == billing_account - - -def test_project_services(plan, project_modules): - "Project service resource must enable APIs specified in the variable." - num_services = len(plan.variables['project_services']) - for mod in project_modules.values(): - project_services = [r for r in mod.resources if r.startswith( - 'google_project_service.project_services')] - assert len(project_services) >= num_services diff --git a/tests/foundations/environments/test_service_accounts.py b/tests/foundations/environments/test_service_accounts.py deleted file mode 100644 index 65640f1a2..000000000 --- a/tests/foundations/environments/test_service_accounts.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -"Test service account creation in root module." - - -import pytest - - -@pytest.fixture(scope='module') -def mod(plan): - return plan.modules['module.service-accounts-tf-environments'] - - -def test_accounts(plan, mod): - "One service account per environment should be created." - environments = plan.variables['environments'] - prefix = plan.variables['prefix'] - resources = [ - v for k, v in mod.resources.items() if 'google_service_account.' in k] - assert len(resources) == len(environments) - assert sorted([res['values']['account_id'] for res in resources]) == sorted([ - '%s-%s' % (prefix, env) for env in environments]) diff --git a/tests/infrastructure/hub_and_spoke_vpns/conftest.py b/tests/infrastructure/hub_and_spoke_vpns/conftest.py deleted file mode 100644 index cb648e15a..000000000 --- a/tests/infrastructure/hub_and_spoke_vpns/conftest.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -"Plan fixture." - -import os - -import pytest - - -_TFDIR = os.path.sep.join(os.path.abspath(__file__).split(os.path.sep)[-3:-1]) - - -@pytest.fixture(scope='package') -def plan(plan): - return plan(_TFDIR) diff --git a/tests/infrastructure/hub_and_spoke_vpns/terraform.tfvars b/tests/infrastructure/hub_and_spoke_vpns/terraform.tfvars deleted file mode 100644 index cfe767c3d..000000000 --- a/tests/infrastructure/hub_and_spoke_vpns/terraform.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -hub_project_id = "automation-examples" -spoke_1_project_id = "automation-examples" -spoke_2_project_id = "automation-examples" diff --git a/tests/infrastructure/hub_and_spoke_vpns/test_cloud_routers.py b/tests/infrastructure/hub_and_spoke_vpns/test_cloud_routers.py deleted file mode 100644 index 8a3dd77a9..000000000 --- a/tests/infrastructure/hub_and_spoke_vpns/test_cloud_routers.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -"Test cloud routers resources creation in root module." - - -def test_hub_custom_routers(plan): - "Hub to spoke routers should match input variables." - for i in (1, 2): - router = plan.resources['google_compute_router.hub-to-spoke-%s-custom[0]' % i] - bgp = router['values']['bgp'][0] - assert bgp['advertise_mode'] == 'CUSTOM' - assert bgp['advertised_groups'] == ['ALL_SUBNETS'] - assert bgp['asn'] == plan.variables['hub_bgp_asn'] - subnet_ranges = [s['subnet_ip'] - for s in plan.variables['spoke_%s_subnets' % (3 - i)]] - assert [r['range'] for r in bgp['advertised_ip_ranges']] == subnet_ranges - - -def test_spoke_routers(plan): - "Spoke routers should match input variables." - for i in (1, 2): - router = plan.resources['google_compute_router.spoke-%s' % i] - bgp = router['values']['bgp'][0] - assert bgp['advertise_mode'] == 'DEFAULT' - assert bgp['advertised_groups'] == None - assert bgp['asn'] == plan.variables['spoke_%s_bgp_asn' % i] diff --git a/tests/infrastructure/hub_and_spoke_vpns/test_firewall.py b/tests/infrastructure/hub_and_spoke_vpns/test_firewall.py deleted file mode 100644 index cbba86f9b..000000000 --- a/tests/infrastructure/hub_and_spoke_vpns/test_firewall.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -"Test firewall resources creation in root module." - - -import pytest - - -@pytest.fixture(scope='module') -def firewall_modules(plan): - return [v for k, v in plan.modules.items() if k.startswith('module.firewall-')] - - -def test_firewall_rules(plan, firewall_modules): - "Test that the hub and spoke VPCs have allow-admin firewall rules" - source_ranges = [] - for k in plan.variables: - if not k.endswith('_subnets'): - continue - source_ranges += [s['subnet_ip'] for s in plan.variables[k]] - for mod in firewall_modules: - allow_admins_resource = mod.resources['google_compute_firewall.allow-admins[0]'] - allow_ssh = mod.resources['google_compute_firewall.allow-tag-ssh[0]'] - assert allow_admins_resource['values']['source_ranges'] == source_ranges - assert allow_ssh['values']['source_ranges'] == ['0.0.0.0/0'] diff --git a/tests/infrastructure/hub_and_spoke_vpns/test_outputs.py b/tests/infrastructure/hub_and_spoke_vpns/test_outputs.py deleted file mode 100644 index 819c0d361..000000000 --- a/tests/infrastructure/hub_and_spoke_vpns/test_outputs.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -"Test root module outputs." - - -def test_hub_outputs(plan): - "Hub VPC ranges and regions should match input variables." - output = plan.outputs['hub'] - for subnet in plan.variables['hub_subnets']: - name = subnet['subnet_name'] - assert output['subnets_ips'][name] == subnet['subnet_ip'] - assert output['subnets_regions'][name] == subnet['subnet_region'] - - -def test_spokes_outputs(plan): - "Spokes VPC ranges and regions should match input variables." - for i in (1, 2): - output = plan.outputs['spoke-%s' % i] - for subnet in plan.variables['spoke_%s_subnets' % i]: - name = subnet['subnet_name'] - assert output['subnets_ips'][name] == subnet['subnet_ip'] - assert output['subnets_regions'][name] == subnet['subnet_region'] diff --git a/tests/infrastructure/hub_and_spoke_vpns/test_vpns.py b/tests/infrastructure/hub_and_spoke_vpns/test_vpns.py deleted file mode 100644 index 5cd0697b5..000000000 --- a/tests/infrastructure/hub_and_spoke_vpns/test_vpns.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -"Test VPN BGP ASNs in root module." - - -import pytest - - -def test_spokes_peer_asn(plan): - "Test that the spoke-to-hub VPNs mach input variables" - mods = [v for k, v in plan.modules.items() if k.startswith('module.vpn-spoke')] - for mod in mods: - bgp_peer = mod.resources['google_compute_router_peer.bgp_peer[0]'] - assert bgp_peer['values']['peer_asn'] == plan.variables['hub_bgp_asn'] - - -def test_hub_peer_asns(plan): - "Test that the hub-to-spoke VPNs mach input variables" - mods = [v for k, v in plan.modules.items() if k.startswith('module.vpn-hub')] - for mod in mods: - bgp_peer = mod.resources['google_compute_router_peer.bgp_peer[0]'] - asn_varname = 'spoke_%s_bgp_asn' % mod['address'][-1] - assert bgp_peer['values']['peer_asn'] == plan.variables[asn_varname] diff --git a/tests/infrastructure/shared_vpc/terraform.tfvars b/tests/infrastructure/shared_vpc/terraform.tfvars deleted file mode 100644 index 4514993ff..000000000 --- a/tests/infrastructure/shared_vpc/terraform.tfvars +++ /dev/null @@ -1,4 +0,0 @@ -root_node = "folders/1234567890" -prefix = "fabric-svpc" -billing_account_id = "012345-012345-012345" -owners_gce = ["user:user@example.com"] diff --git a/tests/infrastructure/shared_vpc/test_outputs.py b/tests/infrastructure/shared_vpc/test_outputs.py deleted file mode 100644 index c8ca16ede..000000000 --- a/tests/infrastructure/shared_vpc/test_outputs.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -"Test root module outputs." - - -def test_vpc_ranges(plan): - "VPC ranges should match input variables." - ranges = plan.outputs['vpc_subnets'] - for subnet in plan.variables['subnets']: - assert ranges[subnet['subnet_name']] == subnet['subnet_ip'] - - -def test_project_ids(plan): - "Project ids should use prefix and match expected values." - prefix = plan.variables['prefix'] - assert plan.outputs['host_project_id'] == prefix + '-vpc-host' - assert plan.outputs['service_project_ids']['gce'] == prefix + '-gce' diff --git a/tests/infrastructure/shared_vpc/test_svpc_resources.py b/tests/infrastructure/shared_vpc/test_svpc_resources.py deleted file mode 100644 index 1e7816069..000000000 --- a/tests/infrastructure/shared_vpc/test_svpc_resources.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.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. - -"Test shared vpc resources in root module." - - -import pytest - - -@pytest.fixture(scope='module') -def mod(plan): - return plan.modules['module.net-svpc-access'] - - -def test_host_vpc(plan): - "Test that the vpc project is set as shared vpc host." - mod = plan.modules['module.net-vpc-host'] - resources = [v['values'] for v in mod.resources.values() if v['type'] == - 'google_compute_shared_vpc_host_project'] - assert resources[0]['project'] == plan.outputs['host_project_id'] - - -def test_service_projects(plan, mod): - "Test that service projects are registered with the shared vpc." - resources = [v['values'] for v in mod.resources.values() if v['type'] == - 'google_compute_shared_vpc_service_project'] - assert len(resources) == 2 - assert set([r['host_project'] for r in resources]) == set( - [plan.outputs['host_project_id']]) - assert sorted([r['service_project'] for r in resources]) == sorted( - plan.outputs['service_project_ids'].values()) - - -def test_subnet_users(plan, mod): - "Test that the network user role is assigned on subnets." - resources = [v['values'] for v in mod.resources.values() if v['type'] == - 'google_compute_subnetwork_iam_binding'] - assert len(resources) == 2 - assert set([r['project'] for r in resources]) == set( - [plan.outputs['host_project_id']]) - assert sorted([r['subnetwork'] for r in resources]) == ['gce', 'gke'] - - -def test_service_agent(plan, mod): - "Test that the service agent role is assigned for gke only." - resources = [v['values'] for v in mod.resources.values() if v['type'] == - 'google_project_iam_binding'] - assert resources[0] == { - 'project': plan.outputs['host_project_id'], - 'role': 'roles/container.hostServiceAgentUser' - } diff --git a/tests/modules/__init__.py b/tests/modules/__init__.py new file mode 100644 index 000000000..3b84e5d22 --- /dev/null +++ b/tests/modules/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + diff --git a/tests/infrastructure/shared_vpc/__init__.py b/tests/modules/compute_vm/__init__.py similarity index 86% rename from tests/infrastructure/shared_vpc/__init__.py rename to tests/modules/compute_vm/__init__.py index 086a24e64..6913f02e3 100644 --- a/tests/infrastructure/shared_vpc/__init__.py +++ b/tests/modules/compute_vm/__init__.py @@ -1,10 +1,10 @@ -# Copyright 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.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 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, diff --git a/tests/modules/compute_vm/fixture/main.tf b/tests/modules/compute_vm/fixture/main.tf new file mode 100644 index 000000000..4f4b3b36d --- /dev/null +++ b/tests/modules/compute_vm/fixture/main.tf @@ -0,0 +1,34 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "test" { + source = "../../../../modules/compute-vm" + project_id = "my-project" + region = "europe-west1" + zone = "europe-west1-b" + name = "test" + network_interfaces = [{ + network = "https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default", + subnetwork = "https://www.googleapis.com/compute/v1/projects/my-project/regions/europe-west1/subnetworks/default-default", + nat = false, + addresses = null + }] + service_account_create = var.service_account_create + instance_count = var.instance_count + use_instance_template = var.use_instance_template + group = var.group + group_manager = var.group_manager +} diff --git a/tests/modules/compute_vm/fixture/variables.tf b/tests/modules/compute_vm/fixture/variables.tf new file mode 100644 index 000000000..7b2b9aad2 --- /dev/null +++ b/tests/modules/compute_vm/fixture/variables.tf @@ -0,0 +1,40 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "group" { + type = any + default = null +} + +variable "group_manager" { + type = any + default = null +} + +variable "instance_count" { + type = number + default = 1 +} + +variable "use_instance_template" { + type = bool + default = false +} + +variable "service_account_create" { + type = bool + default = false +} diff --git a/tests/modules/compute_vm/test_plan.py b/tests/modules/compute_vm/test_plan.py new file mode 100644 index 000000000..009309984 --- /dev/null +++ b/tests/modules/compute_vm/test_plan.py @@ -0,0 +1,46 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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_single_instance(plan_runner): + plan, resources = plan_runner(FIXTURES_DIR) + assert len(resources) == 1 + assert resources[0]['type'] == 'google_compute_instance' + + +def test_multiple_instances(plan_runner): + plan, resources = plan_runner(FIXTURES_DIR, instance_count=2) + assert len(resources) == 2 + assert set(r['type'] for r in resources) == set(['google_compute_instance']) + + +def test_service_account(plan_runner): + plan, resources = plan_runner(FIXTURES_DIR, instance_count=2, + service_account_create='true') + assert len(resources) == 3 + assert 'google_service_account' in [r['type'] for r in resources] + + +def test_template(plan_runner): + plan, resources = plan_runner(FIXTURES_DIR, use_instance_template='true') + assert len(resources) == 1 + assert resources[0]['type'] == 'google_compute_instance_template' + assert resources[0]['values']['name_prefix'] == 'test-' diff --git a/tests/modules/compute_vm/test_plan_group.py b/tests/modules/compute_vm/test_plan_group.py new file mode 100644 index 000000000..de198d731 --- /dev/null +++ b/tests/modules/compute_vm/test_plan_group.py @@ -0,0 +1,44 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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_unmanaged(plan_runner): + plan, resources = plan_runner(FIXTURES_DIR, instance_count=2, + group='{named_ports={}}') + assert len(resources) == 3 + assert set(r['type'] for r in resources) == set([ + 'google_compute_instance_group', 'google_compute_instance' + ]) + + +def test_managed(plan_runner): + plan, resources = plan_runner( + FIXTURES_DIR, use_instance_template='true', group_manager=( + '{ ' + 'auto_healing_policies=null, named_ports={}, options=null, ' + 'regional=false, target_size=1, update_policy=null, versions=null' + ' }' + ) + ) + assert len(resources) == 2 + assert set(r['type'] for r in resources) == set([ + 'google_compute_instance_group_manager', 'google_compute_instance_template' + ]) diff --git a/tests/infrastructure/hub_and_spoke_vpns/__init__.py b/tests/modules/cos_container_coredns/__init__.py similarity index 86% rename from tests/infrastructure/hub_and_spoke_vpns/__init__.py rename to tests/modules/cos_container_coredns/__init__.py index 086a24e64..6913f02e3 100644 --- a/tests/infrastructure/hub_and_spoke_vpns/__init__.py +++ b/tests/modules/cos_container_coredns/__init__.py @@ -1,10 +1,10 @@ -# Copyright 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.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 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, diff --git a/tests/modules/cos_container_coredns/fixture/main.tf b/tests/modules/cos_container_coredns/fixture/main.tf new file mode 100644 index 000000000..82f14f8b8 --- /dev/null +++ b/tests/modules/cos_container_coredns/fixture/main.tf @@ -0,0 +1,24 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "test" { + source = "../../../../modules/cos-container/coredns" + cloud_config = var.cloud_config + config_variables = var.config_variables + coredns_config = var.coredns_config + file_defaults = var.file_defaults + files = var.files +} diff --git a/tests/modules/cos_container_coredns/fixture/outputs.tf b/tests/modules/cos_container_coredns/fixture/outputs.tf new file mode 100644 index 000000000..3354b14a1 --- /dev/null +++ b/tests/modules/cos_container_coredns/fixture/outputs.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "cloud_config" { + value = module.test.cloud_config +} diff --git a/tests/modules/cos_container_coredns/fixture/variables.tf b/tests/modules/cos_container_coredns/fixture/variables.tf new file mode 100644 index 000000000..d291be3aa --- /dev/null +++ b/tests/modules/cos_container_coredns/fixture/variables.tf @@ -0,0 +1,50 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "cloud_config" { + type = string + default = null +} + +variable "config_variables" { + type = map(any) + default = {} +} + +variable "coredns_config" { + type = string + default = null +} + +variable "file_defaults" { + type = object({ + owner = string + permissions = string + }) + default = { + owner = "root" + permissions = "0644" + } +} + +variable "files" { + type = map(object({ + content = string + owner = string + permissions = string + })) + default = {} +} diff --git a/tests/modules/cos_container_coredns/test_apply.py b/tests/modules/cos_container_coredns/test_apply.py new file mode 100644 index 000000000..205c8aaf7 --- /dev/null +++ b/tests/modules/cos_container_coredns/test_apply.py @@ -0,0 +1,34 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 +import re +import yaml + + +FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture') + + +def test_defaults(apply_runner): + "Test defalt configuration." + _, output = apply_runner(FIXTURES_DIR) + cloud_config = output['cloud_config'] + yaml.safe_load(cloud_config) + assert cloud_config.startswith('#cloud-config') + assert re.findall(r'(?m)^\s+\-\s*path:\s*(\S+)', cloud_config) == [ + '/var/lib/docker/daemon.json', '/etc/systemd/resolved.conf', + '/etc/coredns/Corefile', '/etc/systemd/system/coredns.service' + ] diff --git a/tests/foundations/business_units/__init__.py b/tests/modules/cos_container_mysql/__init__.py similarity index 85% rename from tests/foundations/business_units/__init__.py rename to tests/modules/cos_container_mysql/__init__.py index 47be2ee28..6913f02e3 100644 --- a/tests/foundations/business_units/__init__.py +++ b/tests/modules/cos_container_mysql/__init__.py @@ -1,10 +1,10 @@ -# Copyright 2019 Google LLC -# +# Copyright 2020 Google LLC +# # Licensed under the Apache License, Version 2.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 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, diff --git a/tests/modules/cos_container_mysql/fixture/main.tf b/tests/modules/cos_container_mysql/fixture/main.tf new file mode 100644 index 000000000..044314f25 --- /dev/null +++ b/tests/modules/cos_container_mysql/fixture/main.tf @@ -0,0 +1,26 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "test" { + source = "../../../../modules/cos-container/mysql" + cloud_config = var.cloud_config + config_variables = var.config_variables + image = var.image + kms_config = var.kms_config + mysql_config = var.mysql_config + mysql_data_disk = var.mysql_data_disk + mysql_password = var.mysql_password +} diff --git a/tests/modules/cos_container_mysql/fixture/outputs.tf b/tests/modules/cos_container_mysql/fixture/outputs.tf new file mode 100644 index 000000000..3354b14a1 --- /dev/null +++ b/tests/modules/cos_container_mysql/fixture/outputs.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "cloud_config" { + value = module.test.cloud_config +} diff --git a/tests/modules/cos_container_mysql/fixture/variables.tf b/tests/modules/cos_container_mysql/fixture/variables.tf new file mode 100644 index 000000000..c2c81da02 --- /dev/null +++ b/tests/modules/cos_container_mysql/fixture/variables.tf @@ -0,0 +1,54 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "cloud_config" { + type = string + default = null +} + +variable "config_variables" { + type = map(any) + default = {} +} + +variable "image" { + type = string + default = "mysql:5.7" +} + +variable "kms_config" { + type = object({ + project_id = string + keyring = string + location = string + key = string + }) + default = null +} + +variable "mysql_config" { + type = string + default = null +} + +variable "mysql_data_disk" { + type = string + default = null +} + +variable "mysql_password" { + type = string +} diff --git a/tests/modules/cos_container_mysql/test_apply.py b/tests/modules/cos_container_mysql/test_apply.py new file mode 100644 index 000000000..3672a5bb5 --- /dev/null +++ b/tests/modules/cos_container_mysql/test_apply.py @@ -0,0 +1,55 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 +import re +import yaml + + +FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture') + + +def test_defaults(apply_runner): + "Test defalt configuration." + _, output = apply_runner(FIXTURES_DIR, mysql_password='foo') + cloud_config = output['cloud_config'] + yaml.safe_load(cloud_config) + assert cloud_config.startswith('#cloud-config') + assert re.findall(r'(?m)^\s+\-\s*path:\s*(\S+)', cloud_config) == [ + '/var/lib/docker/daemon.json', + '/run/mysql/secrets/mysql-passwd.txt', + '/etc/systemd/system/mysql.service' + ] + assert 'gcloud' not in cloud_config + + +def test_kms(apply_runner): + "Test KMS configuration." + kms_config = ( + '{project_id="my-project", keyring="my-keyring", location="eu", key="foo"}' + ) + _, output = apply_runner( + FIXTURES_DIR, mysql_password='foo', kms_config=kms_config) + cloud_config = output['cloud_config'] + yaml.safe_load(cloud_config) + assert cloud_config.startswith('#cloud-config') + assert re.findall(r'(?m)^\s+\-\s*path:\s*(\S+)', cloud_config) == [ + '/var/lib/docker/daemon.json', + '/run/mysql/secrets/mysql-passwd-cipher.txt', + '/run/mysql/passwd.sh', + '/etc/systemd/system/mysql.service' + ] + assert 'gcloud' in cloud_config diff --git a/tests/modules/dns/__init__.py b/tests/modules/dns/__init__.py new file mode 100644 index 000000000..6913f02e3 --- /dev/null +++ b/tests/modules/dns/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/modules/dns/fixture/main.tf b/tests/modules/dns/fixture/main.tf new file mode 100644 index 000000000..8f6bd0a77 --- /dev/null +++ b/tests/modules/dns/fixture/main.tf @@ -0,0 +1,29 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "test" { + source = "../../../../modules/dns" + project_id = "my-project" + name = "test" + domain = "test.example." + client_networks = [ + "https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default" + ] + type = var.type + forwarders = var.forwarders + peer_network = var.peer_network + recordsets = var.recordsets +} diff --git a/tests/modules/dns/fixture/variables.tf b/tests/modules/dns/fixture/variables.tf new file mode 100644 index 000000000..a00eaeae1 --- /dev/null +++ b/tests/modules/dns/fixture/variables.tf @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "forwarders" { + type = list(string) + default = null +} + +variable "peer_network" { + type = string + default = null +} + +variable "recordsets" { + type = list(object({ + name = string + type = string + ttl = number + records = list(string) + })) + default = [ + { name = "localhost", type = "A", ttl = 300, records = ["127.0.0.1"] }, + { name = "local-host", type = "A", ttl = 300, records = ["127.0.0.2"] } + ] +} + +variable "type" { + type = string + default = "private" +} diff --git a/tests/modules/dns/test_plan.py b/tests/modules/dns/test_plan.py new file mode 100644 index 000000000..135adc95f --- /dev/null +++ b/tests/modules/dns/test_plan.py @@ -0,0 +1,80 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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_private(plan_runner): + "Test private zone with two recordsets." + _, resources = plan_runner(FIXTURES_DIR) + assert len(resources) == 3 + assert set(r['type'] for r in resources) == set([ + 'google_dns_record_set', 'google_dns_managed_zone' + ]) + for r in resources: + if r['type'] != 'google_dns_managed_zone': + continue + assert r['values']['visibility'] == 'private' + assert len(r['values']['private_visibility_config']) == 1 + + +def test_forwarding_recordsets_null_forwarders(plan_runner): + "Test forwarding zone with wrong set of attributes does not break." + _, resources = plan_runner(FIXTURES_DIR, type='forwarding') + assert len(resources) == 1 + resource = resources[0] + assert resource['type'] == 'google_dns_managed_zone' + assert resource['values']['forwarding_config'] == [] + + +def test_forwarding(plan_runner): + "Test forwarding zone with single forwarder." + _, resources = plan_runner( + FIXTURES_DIR, type='forwarding', recordsets='null', + forwarders='["dummy-vpc-self-link"]') + assert len(resources) == 1 + resource = resources[0] + assert resource['type'] == 'google_dns_managed_zone' + assert resource['values']['forwarding_config'] == [{'target_name_servers': [ + {'forwarding_path': '', 'ipv4_address': 'dummy-vpc-self-link'}]}] + + +def test_peering(plan_runner): + "Test peering zone." + _, resources = plan_runner(FIXTURES_DIR, type='peering', + recordsets='null', peer_network='dummy-vpc-self-link') + assert len(resources) == 1 + resource = resources[0] + assert resource['type'] == 'google_dns_managed_zone' + assert resource['values']['peering_config'] == [ + {'target_network': [{'network_url': 'dummy-vpc-self-link'}]}] + + +def test_public(plan_runner): + "Test public zone with two recordsets." + _, resources = plan_runner(FIXTURES_DIR, type='public') + assert len(resources) == 3 + assert set(r['type'] for r in resources) == set([ + 'google_dns_record_set', 'google_dns_managed_zone' + ]) + for r in resources: + if r['type'] != 'google_dns_managed_zone': + continue + assert r['values']['visibility'] == 'public' + assert r['values']['private_visibility_config'] == [] diff --git a/tests/modules/folders/__init__.py b/tests/modules/folders/__init__.py new file mode 100644 index 000000000..6913f02e3 --- /dev/null +++ b/tests/modules/folders/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/modules/folders/fixture/main.tf b/tests/modules/folders/fixture/main.tf new file mode 100644 index 000000000..4848ec5aa --- /dev/null +++ b/tests/modules/folders/fixture/main.tf @@ -0,0 +1,23 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "test" { + source = "../../../../modules/folders" + parent = "organizations/12345678" + names = ["folder-a", "folder-b"] + iam_members = var.iam_members + iam_roles = var.iam_roles +} diff --git a/tests/modules/folders/fixture/variables.tf b/tests/modules/folders/fixture/variables.tf new file mode 100644 index 000000000..02fb11081 --- /dev/null +++ b/tests/modules/folders/fixture/variables.tf @@ -0,0 +1,25 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "iam_members" { + type = map(map(list(string))) + default = null +} + +variable "iam_roles" { + type = map(list(string)) + default = null +} diff --git a/tests/modules/folders/test_plan.py b/tests/modules/folders/test_plan.py new file mode 100644 index 000000000..fcb8fa64d --- /dev/null +++ b/tests/modules/folders/test_plan.py @@ -0,0 +1,49 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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_folder(plan_runner): + "Test folder resources." + _, resources = plan_runner(FIXTURES_DIR) + assert len(resources) == 2 + assert set(r['type'] for r in resources) == set(['google_folder']) + assert set(r['values']['display_name'] for r in resources) == set([ + 'folder-a', 'folder-b' + ]) + assert set(r['values']['parent'] for r in resources) == set([ + 'organizations/12345678' + ]) + + +def test_iam_roles_only(plan_runner): + "Test folder resources with only iam roles passed." + _, resources = plan_runner( + FIXTURES_DIR, iam_roles='{folder-a = [ "roles/owner"]}') + assert len(resources) == 3 + + +def test_iam(plan_runner): + "Test folder resources with iam roles and members." + iam_roles = '{folder-a = ["roles/owner"], folder-b = ["roles/viewer"]}' + iam_members = '{folder-a = { "roles/owner" = ["user:a@b.com"] }}' + _, resources = plan_runner( + FIXTURES_DIR, iam_roles=iam_roles, iam_members=iam_members) + assert len(resources) == 4 diff --git a/tests/modules/gcs/__init__.py b/tests/modules/gcs/__init__.py new file mode 100644 index 000000000..6913f02e3 --- /dev/null +++ b/tests/modules/gcs/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/modules/gcs/fixture/main.tf b/tests/modules/gcs/fixture/main.tf new file mode 100644 index 000000000..c0f4b4cbf --- /dev/null +++ b/tests/modules/gcs/fixture/main.tf @@ -0,0 +1,28 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "test" { + source = "../../../../modules/gcs" + project_id = "my-project" + names = ["bucket-a", "bucket-b"] + prefix = var.prefix + iam_members = var.iam_members + iam_roles = var.iam_roles + labels = var.labels + bucket_policy_only = var.bucket_policy_only + force_destroy = var.force_destroy + versioning = var.versioning +} diff --git a/tests/modules/gcs/fixture/variables.tf b/tests/modules/gcs/fixture/variables.tf new file mode 100644 index 000000000..7caec8c7e --- /dev/null +++ b/tests/modules/gcs/fixture/variables.tf @@ -0,0 +1,55 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "bucket_policy_only" { + type = map(bool) + default = { bucket-a = false } +} + +variable "force_destroy" { + type = map(bool) + default = { bucket-a = true } +} + +variable "iam_members" { + type = map(map(list(string))) + default = null +} + +variable "iam_roles" { + type = map(list(string)) + default = null +} + +variable "labels" { + type = map(string) + default = { environment = "test" } +} + +variable "prefix" { + type = string + default = "" +} + +variable "storage_class" { + type = string + default = "MULTI_REGIONAL" +} + +variable "versioning" { + type = map(bool) + default = { bucket-a = true } +} diff --git a/tests/modules/gcs/test_plan.py b/tests/modules/gcs/test_plan.py new file mode 100644 index 000000000..c2a593439 --- /dev/null +++ b/tests/modules/gcs/test_plan.py @@ -0,0 +1,81 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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_buckets(plan_runner): + "Test bucket resources." + _, resources = plan_runner(FIXTURES_DIR) + assert len(resources) == 2 + assert set(r['type'] for r in resources) == set(['google_storage_bucket']) + assert set(r['values']['name'] for r in resources) == set([ + 'bucket-a', 'bucket-b' + ]) + assert set(r['values']['project'] for r in resources) == set([ + 'my-project' + ]) + + +def test_prefix(plan_runner): + "Test bucket name when prefix is set." + _, resources = plan_runner(FIXTURES_DIR, prefix='foo') + assert set(r['values']['name'] for r in resources) == set([ + 'foo-eu-bucket-a', 'foo-eu-bucket-b' + ]) + + +def test_map_values(plan_runner): + "Test that map values set the correct attributes on buckets." + _, resources = plan_runner(FIXTURES_DIR) + bpo = dict((r['values']['name'], r['values']['bucket_policy_only']) + for r in resources) + assert bpo == {'bucket-a': False, 'bucket-b': True} + force_destroy = dict((r['values']['name'], r['values']['force_destroy']) + for r in resources) + assert force_destroy == {'bucket-a': True, 'bucket-b': False} + versioning = dict((r['values']['name'], r['values']['versioning']) + for r in resources) + assert versioning == { + 'bucket-a': [{'enabled': True}], 'bucket-b': [{'enabled': False}] + } + for r in resources: + assert r['values']['labels'] == { + 'environment': 'test', 'location': 'eu', + 'storage_class': 'multi_regional', 'name': r['values']['name'] + } + + +def test_iam_roles_only(plan_runner): + "Test bucket resources with only iam roles passed." + _, resources = plan_runner( + FIXTURES_DIR, iam_roles='{bucket-a = [ "roles/storage.admin"]}') + assert len(resources) == 3 + + +def test_iam(plan_runner): + "Test bucket resources with iam roles and members." + iam_roles = ( + '{bucket-a = ["roles/storage.admin"], ' + 'bucket-b = ["roles/storage.objectAdmin"]}' + ) + iam_members = '{folder-a = { "roles/storage.admin" = ["user:a@b.com"] }}' + _, resources = plan_runner( + FIXTURES_DIR, iam_roles=iam_roles, iam_members=iam_members) + assert len(resources) == 4 diff --git a/tests/modules/net-vpc/__init__.py b/tests/modules/net-vpc/__init__.py new file mode 100644 index 000000000..6913f02e3 --- /dev/null +++ b/tests/modules/net-vpc/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/modules/net-vpc/fixture/main.tf b/tests/modules/net-vpc/fixture/main.tf new file mode 100644 index 000000000..a9d92d477 --- /dev/null +++ b/tests/modules/net-vpc/fixture/main.tf @@ -0,0 +1,34 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "test" { + source = "../../../../modules/net-vpc" + project_id = var.project_id + name = var.name + iam_members = var.iam_members + iam_roles = var.iam_roles + log_configs = var.log_configs + log_config_defaults = var.log_config_defaults + peering_config = var.peering_config + routes = var.routes + shared_vpc_host = var.shared_vpc_host + shared_vpc_service_projects = var.shared_vpc_service_projects + subnets = var.subnets + subnet_descriptions = var.subnet_descriptions + subnet_flow_logs = var.subnet_flow_logs + subnet_private_access = var.subnet_private_access + auto_create_subnetworks = var.auto_create_subnetworks +} diff --git a/tests/modules/net-vpc/fixture/variables.tf b/tests/modules/net-vpc/fixture/variables.tf new file mode 100644 index 000000000..56f190a20 --- /dev/null +++ b/tests/modules/net-vpc/fixture/variables.tf @@ -0,0 +1,124 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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" { + type = string + default = "my-project" +} + +variable "name" { + type = string + default = "my-vpc" +} + +variable "auto_create_subnetworks" { + type = bool + default = false +} + +variable "iam_roles" { + type = map(list(string)) + default = null +} + +variable "iam_members" { + type = map(map(list(string))) + default = null +} + +variable "log_configs" { + type = map(map(string)) + default = null +} + +variable "log_config_defaults" { + type = object({ + aggregation_interval = string + flow_sampling = number + metadata = string + }) + default = { + aggregation_interval = "INTERVAL_5_SEC" + flow_sampling = 0.5 + metadata = "INCLUDE_ALL_METADATA" + } +} + +variable "peering_config" { + type = object({ + peer_vpc_self_link = string + export_routes = bool + import_routes = bool + }) + default = null +} + +variable "routes" { + type = map(object({ + dest_range = string + priority = number + tags = list(string) + next_hop_type = string # gateway, instance, ip, vpn_tunnel, ilb + next_hop = string + })) + default = null +} + +variable "routing_mode" { + description = "The network routing mode (default 'GLOBAL')" + type = string + default = "GLOBAL" +} + +variable "shared_vpc_host" { + description = "Enable shared VPC for this project." + type = bool + default = false +} + +variable "shared_vpc_service_projects" { + description = "Shared VPC service projects to register with this host" + type = list(string) + default = [] +} + +variable "subnets" { + description = "The list of subnets being created" + type = map(object({ + ip_cidr_range = string + region = string + secondary_ip_range = map(string) + })) + default = null +} + +variable "subnet_descriptions" { + description = "Optional map of subnet descriptions, keyed by subnet name." + type = map(string) + default = {} +} + +variable "subnet_flow_logs" { + description = "Optional map of boolean to control flow logs (default is disabled), keyed by subnet name." + type = map(bool) + default = {} +} + +variable "subnet_private_access" { + description = "Optional map of boolean to control private Google access (default is enabled), keyed by subnet name." + type = map(bool) + default = {} +} diff --git a/tests/modules/net-vpc/test_plan.py b/tests/modules/net-vpc/test_plan.py new file mode 100644 index 000000000..fc2852caa --- /dev/null +++ b/tests/modules/net-vpc/test_plan.py @@ -0,0 +1,90 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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') +_VAR_PEER_VPC_CONFIG = ( + '{' + 'peer_vpc_self_link="projects/my-project/global/networks/my-peer-vpc", ' + 'export_routes=true, import_routes=null' + '}' +) +_VAR_ROUTES_TEMPLATE = ( + '{' + ' next-hop-test = {' + ' dest_range="192.168.128.0/24", priority=1000, tags=null, ' + ' next_hop_type="%s", next_hop="%s"},' + ' gateway-test = {' + ' dest_range="0.0.0.0/0", priority=100, tags=["tag-a"], ' + ' next_hop_type="gateway", ' + ' next_hop="global/gateways/default-internet-gateway"}' + '}' +) +_VAR_ROUTES_NEXT_HOPS = { + 'gateway': 'global/gateways/default-internet-gateway', + 'instance': 'zones/europe-west1-b/test', + 'ip': '192.168.0.128', + 'ilb': 'regions/europe-west1/forwardingRules/test', + 'vpn_tunnel': 'regions/europe-west1/vpnTunnels/foo' +} + + +def test_vpc_simple(plan_runner): + "Test vpc with no extra options." + _, resources = plan_runner(FIXTURES_DIR) + assert len(resources) == 1 + assert [r['type'] for r in resources] == ['google_compute_network'] + assert [r['values']['name'] for r in resources] == ['my-vpc'] + assert [r['values']['project'] for r in resources] == ['my-project'] + + +def test_vpc_shared(plan_runner): + "Test shared vpc variables." + _, resources = plan_runner(FIXTURES_DIR, shared_vpc_host='true', + shared_vpc_service_projects='["tf-a", "tf-b"]') + assert len(resources) == 4 + assert set(r['type'] for r in resources) == set([ + 'google_compute_network', 'google_compute_shared_vpc_host_project', + 'google_compute_shared_vpc_service_project' + ]) + + +def test_vpc_peering(plan_runner): + "Test vpc peering variables." + _, resources = plan_runner(FIXTURES_DIR, peering_config=_VAR_PEER_VPC_CONFIG) + assert len(resources) == 3 + assert set(r['type'] for r in resources) == set([ + 'google_compute_network', 'google_compute_network_peering' + ]) + peerings = [r['values'] + for r in resources if r['type'] == 'google_compute_network_peering'] + assert [p['name'] for p in peerings] == [ + 'my-vpc-my-peer-vpc', 'my-peer-vpc-my-vpc'] + assert [p['export_custom_routes'] for p in peerings] == [True, False] + assert [p['import_custom_routes'] for p in peerings] == [False, True] + + +def test_vpc_routes(plan_runner): + "Test vpc routes." + for next_hop_type, next_hop in _VAR_ROUTES_NEXT_HOPS.items(): + _var_routes = _VAR_ROUTES_TEMPLATE % (next_hop_type, next_hop) + _, resources = plan_runner(FIXTURES_DIR, routes=_var_routes) + assert len(resources) == 3 + resource = [r for r in resources if r['values'] + ['name'] == 'next-hop-test'][0] + assert resource['values']['next_hop_%s' % next_hop_type] diff --git a/tests/modules/net-vpc/test_plan_subnets.py b/tests/modules/net-vpc/test_plan_subnets.py new file mode 100644 index 000000000..f08f42520 --- /dev/null +++ b/tests/modules/net-vpc/test_plan_subnets.py @@ -0,0 +1,79 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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') +_VAR_SUBNETS = ( + '{ ' + 'a={region = "europe-west1", ip_cidr_range = "10.0.0.0/24",' + ' secondary_ip_range=null},' + 'b={region = "europe-west1", ip_cidr_range = "10.0.1.0/24",' + ' secondary_ip_range=null},' + 'c={region = "europe-west1", ip_cidr_range = "10.0.2.0/24",' + ' secondary_ip_range={a="192.168.0.0/24", b="192.168.1.0/24"}},' + '}' +) +_VAR_LOG_CONFIG = '{a = { flow_sampling = 0.1 }}' +_VAR_LOG_CONFIG_DEFAULTS = ( + '{' + 'aggregation_interval = "INTERVAL_10_MIN", ' + 'flow_sampling = 0.5, ' + 'metadata = "INCLUDE_ALL_METADATA"' + '}' +) + + +def test_subnets_simple(plan_runner): + "Test subnets variable." + _, resources = plan_runner(FIXTURES_DIR, subnets=_VAR_SUBNETS) + assert len(resources) == 4 + subnets = [r['values'] + for r in resources if r['type'] == 'google_compute_subnetwork'] + assert set(s['name'] for s in subnets) == set( + ['my-vpc-a', 'my-vpc-b', 'my-vpc-c']) + assert set(len(s['secondary_ip_range']) for s in subnets) == set([0, 0, 2]) + + +def test_subnet_log_configs(plan_runner): + "Test subnets flow logs configuration and defaults." + _, resources = plan_runner(FIXTURES_DIR, subnets=_VAR_SUBNETS, + log_configs=_VAR_LOG_CONFIG, + log_config_defaults=_VAR_LOG_CONFIG_DEFAULTS, + subnet_flow_logs='{a=true, b=true}') + assert len(resources) == 4 + flow_logs = {} + for r in resources: + if r['type'] != 'google_compute_subnetwork': + continue + flow_logs[r['values']['name']] = r['values']['log_config'] + assert flow_logs == { + # enable, override one default option + 'my-vpc-a': [{ + 'aggregation_interval': 'INTERVAL_10_MIN', + 'flow_sampling': 0.1, + 'metadata': 'INCLUDE_ALL_METADATA' + }], + # enable, use defaults + 'my-vpc-b': [{ + 'aggregation_interval': 'INTERVAL_10_MIN', + 'flow_sampling': 0.5, + 'metadata': 'INCLUDE_ALL_METADATA' + }], + # don't enable + 'my-vpc-c': [] + } diff --git a/tests/modules/project/__init__.py b/tests/modules/project/__init__.py new file mode 100644 index 000000000..6913f02e3 --- /dev/null +++ b/tests/modules/project/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/modules/project/fixture/main.tf b/tests/modules/project/fixture/main.tf new file mode 100644 index 000000000..91a838f63 --- /dev/null +++ b/tests/modules/project/fixture/main.tf @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "test" { + source = "../../../../modules/project" + name = "my-project" + billing_account = "12345-12345-12345" + auto_create_network = var.auto_create_network + custom_roles = var.custom_roles + iam_members = var.iam_members + iam_roles = var.iam_roles + iam_additive_members = var.iam_additive_members + iam_additive_roles = var.iam_additive_roles + labels = var.labels + lien_reason = var.lien_reason + oslogin = var.oslogin + oslogin_admins = var.oslogin_admins + oslogin_users = var.oslogin_users + parent = var.parent + prefix = var.prefix + services = var.services +} diff --git a/tests/modules/project/fixture/variables.tf b/tests/modules/project/fixture/variables.tf new file mode 100644 index 000000000..bcf7da90c --- /dev/null +++ b/tests/modules/project/fixture/variables.tf @@ -0,0 +1,85 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "auto_create_network" { + type = bool + default = false +} + +variable "custom_roles" { + type = map(list(string)) + default = {} +} + +variable "iam_members" { + type = map(list(string)) + default = {} +} + +variable "iam_roles" { + type = list(string) + default = [] +} + +variable "iam_additive_members" { + type = map(list(string)) + default = {} +} + +variable "iam_additive_roles" { + type = list(string) + default = [] +} + +variable "labels" { + type = map(string) + default = {} +} + +variable "lien_reason" { + type = string + default = "" +} + +variable "oslogin" { + type = bool + default = false +} + +variable "oslogin_admins" { + type = list(string) + default = [] +} + +variable "oslogin_users" { + type = list(string) + default = [] +} + +variable "parent" { + type = string + default = "folders/12345678" +} + +variable "prefix" { + type = string + default = null +} + +variable "services" { + type = list(string) + default = [] +} diff --git a/tests/modules/project/test_plan.py b/tests/modules/project/test_plan.py new file mode 100644 index 000000000..4c5aba825 --- /dev/null +++ b/tests/modules/project/test_plan.py @@ -0,0 +1,42 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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_prefix(plan_runner): + "Test project id prefix." + _, resources = plan_runner(FIXTURES_DIR) + assert len(resources) == 1 + assert resources[0]['values']['name'] == 'my-project' + _, resources = plan_runner(FIXTURES_DIR, prefix='foo') + assert len(resources) == 1 + assert resources[0]['values']['name'] == 'foo-my-project' + + +def test_parent(plan_runner): + "Test project parent." + _, resources = plan_runner(FIXTURES_DIR) + assert len(resources) == 1 + assert resources[0]['values']['folder_id'] == '12345678' + assert resources[0]['values'].get('org_id') == None + _, resources = plan_runner(FIXTURES_DIR, parent='organizations/12345678') + assert len(resources) == 1 + assert resources[0]['values']['org_id'] == '12345678' + assert resources[0]['values'].get('folder_id') == None diff --git a/tests/requirements.txt b/tests/requirements.txt index a5b3fbc2d..f31307ca2 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,3 +1,3 @@ -pytest>=4.3.1 -pytest-tldr>=0.2.1 -tftest>=1.2.0 +pytest>=4.6.0 +PyYAML>=5.3 +tftest>=1.5.0 diff --git a/tools/tfdoc/tfdoc.py b/tools/tfdoc/tfdoc.py index bb6dffb03..1e3fbf2c8 100755 --- a/tools/tfdoc/tfdoc.py +++ b/tools/tfdoc/tfdoc.py @@ -16,6 +16,7 @@ import collections import enum +import glob import os import re import string @@ -241,12 +242,16 @@ def replace_doc(module, doc): def main(module=None, replace=True): "Program entry point." try: - with open(os.path.join(module, 'variables.tf')) as file: - variables = [v for v in parse_items( - file.read(), RE_VARIABLES, VariableToken, Variable, VariableData)] - with open(os.path.join(module, 'outputs.tf')) as file: - outputs = [o for o in parse_items( - file.read(), RE_OUTPUTS, OutputToken, Output, OutputData)] + variables = [] + for path in glob.glob(os.path.join(module, 'variables*tf')): + with open(path) as file: + variables += [v for v in parse_items( + file.read(), RE_VARIABLES, VariableToken, Variable, VariableData)] + outputs = [] + for path in glob.glob(os.path.join(module, 'outputs*tf')): + with open(path) as file: + outputs += [o for o in parse_items( + file.read(), RE_OUTPUTS, OutputToken, Output, OutputData)] except (IOError, OSError) as e: raise SystemExit(e) doc = get_doc(variables, outputs)