Service directory DNS example (#120)

* dns service directory example

* fix module references in asset feeds example

* update diagram

* update diagram

* commands

* link Medium article in asset inventory examples' README

* finish service directory dns example
This commit is contained in:
Ludovico Magnocavallo 2020-08-01 18:26:50 +02:00 committed by GitHub
parent 482f4464f8
commit d8b8a5396d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 368 additions and 5 deletions

View File

@ -9,6 +9,8 @@ The Cloud Function can then be used for different purposes:
- adapting the configuration of separate related resources
- implementing remediation steps that enforce policy compliance by tweaking or reverting the changes.
A [companion Medium article](https://medium.com/google-cloud/using-cloud-asset-inventory-feeds-for-dynamic-configuration-and-policy-enforcement-c37b6a590c49) has been published for this example, refer to it for more details on the context and the specifics of running the example.
This example shows a simple remediation use case: how to enforce policies on instance tags and revert non-compliant changes in near-real time, thus adding an additional measure of control when using tags for firewall rule scoping. Changing the [monitored asset](https://cloud.google.com/asset-inventory/docs/supported-asset-types) and the function logic allows simple adaptation to other common use cases:
- enforcing a centrally defined Cloud Armor policy in backend services

View File

@ -20,7 +20,7 @@ locals {
}
module "project" {
source = "github.com/terraform-google-modules/cloud-foundation-fabric//modules/project?ref=v2.3.0"
source = "../../modules/project"
name = var.project_id
project_create = false
services = [
@ -43,7 +43,7 @@ module "project" {
}
module "vpc" {
source = "github.com/terraform-google-modules/cloud-foundation-fabric//modules/net-vpc?ref=v2.3.0"
source = "../../modules/net-vpc"
project_id = module.project.project_id
name = var.name
subnets = [{
@ -55,7 +55,7 @@ module "vpc" {
}
module "pubsub" {
source = "github.com/terraform-google-modules/cloud-foundation-fabric//modules/pubsub?ref=v2.3.0"
source = "../../modules/pubsub"
project_id = module.project.project_id
name = var.name
subscriptions = { "${var.name}-default" = null }
@ -70,14 +70,14 @@ module "pubsub" {
}
module "service-account" {
source = "github.com/terraform-google-modules/cloud-foundation-fabric//modules/iam-service-accounts?ref=v2.3.0"
source = "../../modules/iam-service-accounts"
project_id = module.project.project_id
names = ["${var.name}-cf"]
iam_project_roles = { (module.project.project_id) = [local.role_id] }
}
module "cf" {
source = "github.com/terraform-google-modules/cloud-foundation-fabric//modules/cloud-function?ref=v2.3.0"
source = "../../modules/cloud-function"
project_id = module.project.project_id
name = var.name
bucket_name = "${var.name}-${random_pet.random.id}"

View File

@ -0,0 +1,121 @@
# Fine-grained Cloud DNS IAM via Service Directory
This example shows how to leverage [Service Directory](https://cloud.google.com/blog/products/networking/introducing-service-directory) and Cloud DNS Service Directory private zones, to implement fine-grained IAM controls on DNS.
<!-- A [companion Medium article](https://medium.com/google-cloud/using-cloud-asset-inventory-feeds-for-dynamic-configuration-and-policy-enforcement-c37b6a590c49) has been published for this example, refer to it for more details on the context and the specifics of running the example. -->
This example:
- creates a Service Directory namespace with two services and their endpoints
- creates a Cloud DNS private zone that uses the namespace as its authoritative source
- creates two service accounts and assigns them the `roles/servicedirectory.editor` role on the namespace and on one service respectively
- creates two VMs and sets them to use the two service accounts, so that DNS queries and `gcloud` commands can be used to verify the setup
The resources created in this example are shown in the high level diagram below:
<img src="diagram.png" width="640px">
## Running the example
Clone this repository or [open it in cloud shell](https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2Fterraform-google-modules%2Fcloud-foundation-fabric&cloudshell_print=cloud-shell-readme.txt&cloudshell_working_dir=cloud-operations%2Fcloud-operations/dns-fine-grained-iam), then go through the following steps to create resources:
- `terraform init`
- `terraform apply -var project_id=my-project-id`
Once done testing, you can clean up resources by running `terraform destroy`. To persist state, check out the `backend.tf.sample` file.
## Testing the example
The terraform outputs generate preset `gcloud compute ssh` commands that you can copy and run in the console to connect to each VM. Remember to adapt the testing commands below if you changed the default values for the `name`, `region`, or `zone_domain` variables.
Connect via SSH to the `ns` VM and query the Service Directory namespace via DNS.
```bash
gcloud compute ssh dns-sd-test-ns-1 \
--zone europe-west1-b \
--tunnel-through-iap
dig app1.svc.example.org +short
# 127.0.0.2
# 127.0.0.3
# 127.0.0.7
dig app2.svc.example.org +short
# 127.0.0.4
# 127.0.0.5
dig _app1._tcp.app1.svc.example.org srv +short
# 10 10 80 vm1.app1.svc.example.org.
# 10 10 80 vm2.app1.svc.example.org.
# 10 10 80 vm3.app1.svc.example.org.
```
The DNS answers should match the ones in the comments above, after each command. Note the special format used to query `SRV` records.
If the above looks good, let's verify that the `ns` VM service account has edit rights on the namespace by creating a new service, and then verifying it via DNS.
```bash
gcloud beta service-directory services create app3 \
--location europe-west1 \
--namespace dns-sd-test
# Created service [app3].
gcloud beta service-directory endpoints create vm1 \
--service app3 \
--location europe-west1 \
--namespace dns-sd-test \
--address 127.0.0.6 \
--port 80
# Created endpoint [vm1].
dig app3.svc.example.org +short
# 127.0.0.6
```
Log out from the `ns` VM and log in to the `svc` VM, then verify that its service account has no permissions on the whole namespace.
```bash
gcloud compute ssh dns-sd-test-svc-1 \
--zone europe-west1-b \
--tunnel-through-iap
gcloud beta service-directory services delete app3 \
--location europe-west1 \
--namespace dns-sd-test
# Deleted service [app3].
# ERROR: (gcloud.beta.service-directory.services.delete) PERMISSION_DENIED: Permission 'servicedirectory.services.delete' denied on resource 'projects/my-project/locations/europe-west1/namespaces/dns-sd-test/services/app3'.
```
Ignoring the `deleted` message which is clearly a bug (the service is still in beta after all), the error message shows that this identity has no rights to operate on the namespace. What it can do is operate on the single service we gave it access to.
```bash
gcloud beta service-directory endpoints create vm3 \
--service app1 \
--location europe-west1 \
--namespace dns-sd-test \
--address 127.0.0.7 \
--port 80
# Created endpoint [vm3].
dig app1.svc.example.org +short
# 127.0.0.2
# 127.0.0.3
# 127.0.0.7
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---: |:---:|:---:|
| project_id | Existing project id. | <code title="">string</code> | ✓ | |
| *name* | Arbitrary string used to name created resources. | <code title="">string</code> | | <code title="">dns-sd-test</code> |
| *region* | Compute region used in the example. | <code title="">string</code> | | <code title="">europe-west1</code> |
| *zone_domain* | Domain name used for the DNS zone. | <code title="">string</code> | | <code title="">svc.example.org.</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| gcloud_commands | Commands used to SSH to the VMs. | |
| vms | VM names. | |
<!-- END TFDOC -->

View File

@ -0,0 +1,23 @@
# 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.
# set a valid bucket below and rename this file to backend.tf
terraform {
backend "gcs" {
bucket = ""
prefix = "fabric/operations/service-directory-dns"
}
}

View File

@ -0,0 +1,9 @@
################################# Quickstart #################################
- terraform init
- terraform apply -var project_id=$GOOGLE_CLOUD_PROJECT
Refer to the README.md file for more info and testing flow.

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -0,0 +1,139 @@
/**
* 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 {
startup-script = <<END
#!/bin/bash
apt install -y bash-completion dnsutils
END
}
module "project" {
source = "../../modules/project"
name = var.project_id
project_create = false
services = [
"compute.googleapis.com",
"dns.googleapis.com",
"servicedirectory.googleapis.com"
]
service_config = {
disable_on_destroy = false, disable_dependent_services = false
}
}
module "vpc" {
source = "../../modules/net-vpc"
project_id = module.project.project_id
name = var.name
subnets = [{
ip_cidr_range = "192.168.0.0/24"
name = "${var.name}-default"
region = var.region
secondary_ip_range = {}
}]
}
module "firewall-a" {
source = "../../modules/net-vpc-firewall"
project_id = module.project.project_id
network = module.vpc.name
}
module "nat-a" {
source = "../../modules/net-cloudnat"
project_id = module.project.project_id
region = var.region
name = var.name
router_network = module.vpc.name
}
module "dns-service-zone" {
source = "../../modules/dns"
project_id = module.project.project_id
type = "service-directory"
name = var.name
domain = var.zone_domain
client_networks = [module.vpc.self_link]
service_directory_namespace = module.service-directory.id
}
module "service-directory" {
source = "../../modules/service-directory"
project_id = module.project.project_id
location = var.region
name = var.name
iam_members = {
"roles/servicedirectory.editor" = [
module.vm-ns-editor.service_account_iam_email
]
}
iam_roles = ["roles/servicedirectory.editor"]
services = {
app1 = { endpoints = ["vm1", "vm2"], metadata = null }
app2 = { endpoints = ["vm1", "vm2"], metadata = null }
}
service_iam_members = {
app1 = {
"roles/servicedirectory.editor" = [
module.vm-svc-editor.service_account_iam_email
]
}
}
service_iam_roles = {
app1 = ["roles/servicedirectory.editor"]
}
endpoint_config = {
"app1/vm1" = { address = "127.0.0.2", port = 80, metadata = {} }
"app1/vm2" = { address = "127.0.0.3", port = 80, metadata = {} }
"app2/vm1" = { address = "127.0.0.4", port = 80, metadata = {} }
"app2/vm2" = { address = "127.0.0.5", port = 80, metadata = {} }
}
}
module "vm-ns-editor" {
source = "../../modules/compute-vm"
project_id = module.project.project_id
region = var.region
name = "${var.name}-ns"
network_interfaces = [{
network = module.vpc.self_link,
subnetwork = module.vpc.subnet_self_links["${var.region}/${var.name}-default"],
nat = false,
addresses = null
}]
metadata = { startup-script = local.startup-script }
service_account_create = true
tags = ["ssh"]
instance_count = 1
}
module "vm-svc-editor" {
source = "../../modules/compute-vm"
project_id = module.project.project_id
region = var.region
name = "${var.name}-svc"
network_interfaces = [{
network = module.vpc.self_link,
subnetwork = module.vpc.subnet_self_links["${var.region}/${var.name}-default"],
nat = false,
addresses = null
}]
metadata = { startup-script = local.startup-script }
service_account_create = true
tags = ["ssh"]
instance_count = 1
}

View File

@ -0,0 +1,31 @@
/**
* 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 "vms" {
description = "VM names."
value = {
ns-editor = module.vm-ns-editor.names.0
svc-editor = module.vm-svc-editor.names.0
}
}
output "gcloud_commands" {
description = "Commands used to SSH to the VMs."
value = {
ns-editor = "gcloud compute ssh ${module.vm-ns-editor.names.0} --zone ${var.region}-b --tunnel-through-iap"
svc-editor = "gcloud compute ssh ${module.vm-svc-editor.names.0} --zone ${var.region}-b --tunnel-through-iap"
}
}

View File

@ -0,0 +1,38 @@
/**
* 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 "name" {
description = "Arbitrary string used to name created resources."
type = string
default = "dns-sd-test"
}
variable "project_id" {
description = "Existing project id."
type = string
}
variable "region" {
description = "Compute region used in the example."
type = string
default = "europe-west1"
}
variable "zone_domain" {
description = "Domain name used for the DNS zone."
type = string
default = "svc.example.org."
}