From 2844944a0d9736c7b7cb355a3d7edf8113a6030c Mon Sep 17 00:00:00 2001 From: Dan Vaida Date: Wed, 1 Apr 2020 11:40:53 +0200 Subject: [PATCH] Adds support for attaching to a TGW selected by its name (#2) * Adds support for attaching to a TGW selected by its name * Adjusts CI execution * Corrects source path for test case * Fixes error when TGW ID is passed explicitly --- .github/workflows/terraform.yml | 8 +++- .gitignore | 30 ++++++++++++ Makefile | 59 +++++++++++++++++++++--- README.md | 16 ++++--- data.tf | 38 +++++++++++++++ examples/satellite/README.md | 28 +++++++++++ examples/satellite/locals.tf | 6 +++ examples/satellite/main.tf | 26 +++++++++++ examples/satellite/providers.tf | 19 ++++++++ examples/satellite/variables.auto.tfvars | 12 +++++ examples/satellite/variables.tf | 58 +++++++++++++++++++++++ locals.tf | 4 ++ main.tf | 10 ++-- variables.tf | 7 +++ 14 files changed, 303 insertions(+), 18 deletions(-) create mode 100644 .gitignore create mode 100644 examples/satellite/README.md create mode 100644 examples/satellite/locals.tf create mode 100644 examples/satellite/main.tf create mode 100644 examples/satellite/providers.tf create mode 100644 examples/satellite/variables.auto.tfvars create mode 100644 examples/satellite/variables.tf diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index f33315a..d153cd7 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -40,4 +40,10 @@ jobs: - name: "make test" run: | - make test ARGS="-var assumer_account_role_name=test" + make test ARGS="-var aws_account_id_hub=${TF_VAR_aws_account_id_hub} -var aws_account_id_satellite=[${TF_VAR_aws_account_id_satellite}]" + env: + AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + TF_VAR_aws_account_id_hub: ${{ secrets.aws_account_id_hub }} + TF_VAR_aws_account_id_satellite: ${{ secrets.aws_account_id_satellite }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97afa46 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log + +# Ignore any .tfvars files that are generated automatically for each Terraform run. Most +# .tfvars files are managed as part of configuration and so should be included in +# version control. +# +# example.tfvars + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* +*tfplan* diff --git a/Makefile b/Makefile index d8f229e..0adab54 100644 --- a/Makefile +++ b/Makefile @@ -77,7 +77,7 @@ test: _pull-tf echo "------------------------------------------------------------"; \ echo "# Terraform init"; \ echo "------------------------------------------------------------"; \ - if docker run -it --rm -v "$(CURRENT_DIR):/t" --workdir "$${DOCKER_PATH}" hashicorp/terraform:$(TF_VERSION) \ + if docker run $$(tty -s && echo "-it" || echo) --rm -v "$(CURRENT_DIR):/t" --workdir "$${DOCKER_PATH}" hashicorp/terraform:$(TF_VERSION) \ init \ -verify-plugins=true \ -lock=false \ @@ -90,25 +90,72 @@ test: _pull-tf echo "OK"; \ else \ echo "Failed"; \ - docker run -it --rm -v "$(CURRENT_DIR):/t" --workdir "$${DOCKER_PATH}" --entrypoint=rm hashicorp/terraform:$(TF_VERSION) -rf .terraform/ || true; \ + docker run $$(tty -s && echo "-it" || echo) --rm -v "$(CURRENT_DIR):/t" --workdir "$${DOCKER_PATH}" --entrypoint=rm hashicorp/terraform:$(TF_VERSION) -rf .terraform/ || true; \ exit 1; \ fi; \ echo; \ echo "------------------------------------------------------------"; \ echo "# Terraform validate"; \ echo "------------------------------------------------------------"; \ - if docker run -it --rm -v "$(CURRENT_DIR):/t" --workdir "$${DOCKER_PATH}" hashicorp/terraform:$(TF_VERSION) \ + if docker run $$(tty -s && echo "-it" || echo) --rm -v "$(CURRENT_DIR):/t" --workdir "$${DOCKER_PATH}" hashicorp/terraform:$(TF_VERSION) \ validate \ - $(ARGS) \ .; then \ echo "OK"; \ - docker run -it --rm -v "$(CURRENT_DIR):/t" --workdir "$${DOCKER_PATH}" --entrypoint=rm hashicorp/terraform:$(TF_VERSION) -rf .terraform/ || true; \ else \ echo "Failed"; \ - docker run -it --rm -v "$(CURRENT_DIR):/t" --workdir "$${DOCKER_PATH}" --entrypoint=rm hashicorp/terraform:$(TF_VERSION) -rf .terraform/ || true; \ + docker run $$(tty -s && echo "-it" || echo) --rm -v "$(CURRENT_DIR):/t" --workdir "$${DOCKER_PATH}" --entrypoint=rm hashicorp/terraform:$(TF_VERSION) -rf .terraform/ || true; \ + exit 1; \ + fi; \ + echo; \ + echo "------------------------------------------------------------"; \ + echo "# Terraform plan"; \ + echo "------------------------------------------------------------"; \ + if docker run $$(tty -s && echo "-it" || echo) --rm -v "$(CURRENT_DIR):/t" -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY --workdir "$${DOCKER_PATH}" hashicorp/terraform:$(TF_VERSION) \ + plan \ + $(ARGS) \ + -out=tfplan \ + ; then \ + echo "OK"; \ + else \ + echo "Failed"; \ + docker run $$(tty -s && echo "-it" || echo) --rm -v "$(CURRENT_DIR):/t" --workdir "$${DOCKER_PATH}" --entrypoint=rm hashicorp/terraform:$(TF_VERSION) -rf .terraform/ || true; \ exit 1; \ fi; \ echo; \ + echo "------------------------------------------------------------"; \ + echo "# Terraform apply & destroy"; \ + echo "------------------------------------------------------------"; \ + if docker run $$(tty -s && echo "-it" || echo) --rm -v "$(CURRENT_DIR):/t" -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY --workdir "$${DOCKER_PATH}" hashicorp/terraform:$(TF_VERSION) \ + apply \ + -auto-approve \ + tfplan \ + ; then \ + echo "Apply OK"; \ + if docker run $$(tty -s && echo "-it" || echo) --rm -v "$(CURRENT_DIR):/t" -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY --workdir "$${DOCKER_PATH}" hashicorp/terraform:$(TF_VERSION) \ + destroy \ + -auto-approve \ + $(ARGS) \ + ; then \ + echo "Destroy OK"; \ + docker run $$(tty -s && echo "-it" || echo) --rm -v "$(CURRENT_DIR):/t" --workdir "$${DOCKER_PATH}" --entrypoint=rm hashicorp/terraform:$(TF_VERSION) -rf .terraform/ terraform.tfstate terraform.tfstate.backup || true; \ + else \ + echo "Destroy failed. You should check for dangling resources."; \ + exit 1; \ + fi; \ + else \ + echo "Apply failed"; \ + if docker run -$$(tty -s && echo "-it" || echo) --rm -v "$(CURRENT_DIR):/t" -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY --workdir "$${DOCKER_PATH}" hashicorp/terraform:$(TF_VERSION) \ + destroy \ + -auto-approve \ + $(ARGS) \ + ; then \ + echo "Destroy OK"; \ + docker run -$$(tty -s && echo "-it" || echo) --rm -v "$(CURRENT_DIR):/t" --workdir "$${DOCKER_PATH}" --entrypoint=rm hashicorp/terraform:$(TF_VERSION) -rf .terraform/ terraform.tfstate terraform.tfstate.backup || true; \ + else \ + echo "Destroy failed. You should check for dangling resources."; \ + exit 1; \ + fi; \ + fi; \ ) diff --git a/README.md b/README.md index f8e3080..ea992a2 100644 --- a/README.md +++ b/README.md @@ -56,13 +56,14 @@ Check the `subnet_name_keyword_selector` variable if you want to change this. |------|-------------|------|---------|:-----:| | aws\_account\_id\_hub | AWS account number containing the TGW hub | `string` | n/a | yes | | aws\_login\_profile | Name of the AWS login profile as seen under ~/.aws/config used for assuming cross-account roles | `any` | n/a | yes | -| ram\_resource\_association\_id | Identifier of the Resource Access Manager Resource Association | `string` | n/a | yes | | role\_to\_assume\_hub | IAM role name to assume in the AWS account containing the TGW hub (eg. ASSUME-ROLE-HUB) | `string` | n/a | yes | | aws\_account\_id\_satellite | AWS account number containing the TGW satellite | `string` | `""` | no | | destination\_cidr\_block | CIDR to be routed | `string` | `""` | no | +| ram\_resource\_association\_id | Identifier of the Resource Access Manager Resource Association | `string` | `""` | no | | role\_to\_assume\_satellite | IAM role name to assume in the AWS account containing the TGW satellite (eg. ASSUME-ROLE-SATELLITE) | `string` | `""` | no | | satellite\_create | Boolean flag for toggling the handling of satellite resources | `bool` | `false` | no | | subnet\_name\_keyword\_selector | Keyword matching the name of the subnet(s) for which the routing will be added (i.e. private) | `string` | `"private"` | no | +| transit\_gateway\_hub\_name | Name of the Transit Gateway to attach to | `string` | `""` | no | | transit\_gateway\_id | Identifier of the Transit Gateway | `string` | `""` | no | | transit\_gateway\_route\_table\_id | Identifier of the Transit Gateway Route Table | `string` | `""` | no | | vpc\_name\_to\_attach | Name of the satellite VPC to be attached to the TGW | `string` | `""` | no | @@ -75,14 +76,17 @@ Check the `subnet_name_keyword_selector` variable if you want to change this. +## To do + +- Collect TGW ID directly rather than using a RAM data source + ([currently not supported][7]) +- Add support for VPN attachments +- Add support for passing IDs of subnets while fetching routing table IDs + [1]: https://en.wikipedia.org/wiki/Star_network [2]: https://github.com/Flaconi/terraform-aws-transit-gateway-hub [3]: https://github.com/Flaconi/terraform-aws-transit-gateway-hub/tree/master/examples [4]: https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role.html#examples [5]: https://www.terraform.io/docs/configuration/modules.html#passing-providers-explicitly [6]: https://www.terraform.io/docs/providers/aws/index.html#authentication - -## To do - -- Add support for passing the IDs of the subnets as an input variable -- Add support for VPN attachments +[7]: https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-transit-gateways.html#options diff --git a/data.tf b/data.tf index f7c5d21..47ee5d0 100644 --- a/data.tf +++ b/data.tf @@ -22,3 +22,41 @@ data "aws_route_table" "this" { subnet_id = sort(data.aws_subnet_ids.this[0].ids)[count.index] } + +data "aws_ec2_transit_gateway" "this" { + provider = aws.hub + count = local.create && var.transit_gateway_hub_name != "" ? 1 : 0 + + filter { + name = "state" + values = ["available"] + } + + filter { + name = "owner-id" + values = [var.aws_account_id_hub] + } + + filter { + name = "transit-gateway-id" + values = [data.aws_ram_resource_share.this[0].tags.transit-gateway-id] + } +} + +data "aws_ec2_transit_gateway_route_table" "this" { + provider = aws.hub + count = local.create && var.transit_gateway_hub_name != "" ? 1 : 0 + + filter { + name = "transit-gateway-id" + values = [data.aws_ec2_transit_gateway.this[0].id] + } +} + +data "aws_ram_resource_share" "this" { + provider = aws.hub + count = local.create && var.transit_gateway_hub_name != "" ? 1 : 0 + + name = var.transit_gateway_hub_name + resource_owner = "SELF" +} diff --git a/examples/satellite/README.md b/examples/satellite/README.md new file mode 100644 index 0000000..42a79ee --- /dev/null +++ b/examples/satellite/README.md @@ -0,0 +1,28 @@ +# Standalone invocation of the Transit Gateway satellite module + + +## Providers + +No provider. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:-----:| +| aws\_account\_id\_hub | AWS account number containing the TGW hub | `string` | n/a | yes | +| aws\_account\_id\_satellite | List of AWS account numbers representing the satellites of the TGW | `list` | n/a | yes | +| aws\_login\_profile | Name of the AWS login profile as seen under ~/.aws/config used for assuming cross-account roles | `any` | n/a | yes | +| role\_to\_assume\_hub | IAM role name to assume in the AWS account containing the TGW hub (eg. ASSUME-ROLE-HUB) | `string` | n/a | yes | +| role\_to\_assume\_satellite | IAM role name to assume in the AWS account containing the TGW satellite (eg. ASSUME-ROLE-SATELLITE) | `string` | n/a | yes | +| destination\_cidr\_block | CIDR to be routed | `string` | `""` | no | +| satellite\_create | Boolean flag for toggling the handling of satellite resources | `bool` | `false` | no | +| subnet\_name\_keyword\_selector | Keyword matching the name of the subnet(s) for which the routing will be added (i.e. private) | `string` | `"private"` | no | +| transit\_gateway\_hub\_name | Name of the Transit Gateway to attach to | `string` | `""` | no | +| transit\_gateway\_id | Identifier of the Transit Gateway | `string` | `""` | no | +| vpc\_name\_to\_attach | Name of the satellite VPC to be attached to the TGW | `string` | `""` | no | + +## Outputs + +No output. + + diff --git a/examples/satellite/locals.tf b/examples/satellite/locals.tf new file mode 100644 index 0000000..3fa5aed --- /dev/null +++ b/examples/satellite/locals.tf @@ -0,0 +1,6 @@ +# Workaround for this error when passing undeclared vars using CI/CD +# https://github.com/hashicorp/terraform/issues/22004 +# TODO: refactor it after issue is resolved +locals { + aws_account_id_satellite = var.aws_account_id_satellite[0] +} diff --git a/examples/satellite/main.tf b/examples/satellite/main.tf new file mode 100644 index 0000000..6bb095c --- /dev/null +++ b/examples/satellite/main.tf @@ -0,0 +1,26 @@ +# The Transit Gateway (hub) has already been created in AWS, as a fixture for +# this test case due to not being able to use 'depends_on' on Terraform modules +module "tgw-satellite" { + source = "../../" + + providers = { + aws.satellite = aws.satellite + aws.hub = aws.hub + } + + aws_login_profile = var.aws_login_profile + satellite_create = var.satellite_create + + aws_account_id_hub = var.aws_account_id_hub + aws_account_id_satellite = local.aws_account_id_satellite + + role_to_assume_hub = var.role_to_assume_hub + role_to_assume_satellite = var.role_to_assume_satellite + + vpc_name_to_attach = var.vpc_name_to_attach + destination_cidr_block = var.destination_cidr_block + + subnet_name_keyword_selector = var.subnet_name_keyword_selector + + transit_gateway_hub_name = var.transit_gateway_hub_name +} diff --git a/examples/satellite/providers.tf b/examples/satellite/providers.tf new file mode 100644 index 0000000..4d3606d --- /dev/null +++ b/examples/satellite/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + alias = "satellite" + region = "eu-central-1" + profile = var.aws_login_profile + assume_role { + role_arn = "arn:aws:iam::${local.aws_account_id_satellite}:role/${var.role_to_assume_satellite}" + session_name = "tf-tgw-module-satellite" + } +} + +provider "aws" { + alias = "hub" + region = "eu-central-1" + profile = var.aws_login_profile + assume_role { + role_arn = "arn:aws:iam::${var.aws_account_id_hub}:role/${var.role_to_assume_hub}" + session_name = "tf-tgw-module-satellite" + } +} diff --git a/examples/satellite/variables.auto.tfvars b/examples/satellite/variables.auto.tfvars new file mode 100644 index 0000000..76c2009 --- /dev/null +++ b/examples/satellite/variables.auto.tfvars @@ -0,0 +1,12 @@ +satellite_create = true + +aws_login_profile = "login" + +role_to_assume_hub = "ASSUME-ENG-CI" +role_to_assume_satellite = "ASSUME-ENG-CI" + +vpc_name_to_attach = "default" +destination_cidr_block = "1.1.1.1/32" + +subnet_name_keyword_selector = "private" +transit_gateway_hub_name = "test-tgw-fixture" diff --git a/examples/satellite/variables.tf b/examples/satellite/variables.tf new file mode 100644 index 0000000..6faab81 --- /dev/null +++ b/examples/satellite/variables.tf @@ -0,0 +1,58 @@ +variable "satellite_create" { + description = "Boolean flag for toggling the handling of satellite resources" + default = false + type = bool +} + +variable "aws_login_profile" { + description = "Name of the AWS login profile as seen under ~/.aws/config used for assuming cross-account roles" +} + +variable "aws_account_id_hub" { + description = "AWS account number containing the TGW hub" + type = string +} + +variable "aws_account_id_satellite" { + description = "List of AWS account numbers representing the satellites of the TGW" + type = list +} + +variable "role_to_assume_hub" { + description = "IAM role name to assume in the AWS account containing the TGW hub (eg. ASSUME-ROLE-HUB)" + type = string +} + +variable "role_to_assume_satellite" { + description = "IAM role name to assume in the AWS account containing the TGW satellite (eg. ASSUME-ROLE-SATELLITE)" + type = string +} + +variable "vpc_name_to_attach" { + description = "Name of the satellite VPC to be attached to the TGW" + type = string + default = "" +} + +variable "destination_cidr_block" { + description = "CIDR to be routed" + default = "" +} + +variable "subnet_name_keyword_selector" { + description = "Keyword matching the name of the subnet(s) for which the routing will be added (i.e. private)" + type = string + default = "private" +} + +variable "transit_gateway_hub_name" { + description = "Name of the Transit Gateway to attach to" + type = string + default = "" +} + +variable "transit_gateway_id" { + description = "Identifier of the Transit Gateway" + type = string + default = "" +} diff --git a/locals.tf b/locals.tf index 3574df7..a35331b 100644 --- a/locals.tf +++ b/locals.tf @@ -1,3 +1,7 @@ locals { create = var.satellite_create + + transit_gateway_id = var.transit_gateway_id == "" ? data.aws_ec2_transit_gateway.this[0].id : var.transit_gateway_id + + transit_gateway_route_table_id = var.transit_gateway_route_table_id == "" ? data.aws_ec2_transit_gateway_route_table.this[0].id : var.transit_gateway_route_table_id } diff --git a/main.tf b/main.tf index ac808f1..8e452c3 100644 --- a/main.tf +++ b/main.tf @@ -5,7 +5,7 @@ resource "aws_ec2_transit_gateway_vpc_attachment" "this" { provider = aws.satellite count = local.create ? 1 : 0 subnet_ids = data.aws_subnet_ids.this[0].ids - transit_gateway_id = var.transit_gateway_id + transit_gateway_id = local.transit_gateway_id vpc_id = data.aws_vpc.this[0].id # When we create the TGW and the association through RAM in one run, we need @@ -18,7 +18,7 @@ resource "aws_ec2_transit_gateway_route" "this" { count = local.create ? 1 : 0 destination_cidr_block = var.destination_cidr_block transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.this[0].id - transit_gateway_route_table_id = var.transit_gateway_route_table_id + transit_gateway_route_table_id = local.transit_gateway_route_table_id depends_on = [aws_ec2_transit_gateway_vpc_attachment.this] } @@ -26,7 +26,7 @@ resource "aws_ec2_transit_gateway_route_table_association" "this" { provider = aws.hub count = local.create ? 1 : 0 transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.this[0].id - transit_gateway_route_table_id = var.transit_gateway_route_table_id + transit_gateway_route_table_id = local.transit_gateway_route_table_id depends_on = [aws_ec2_transit_gateway_vpc_attachment.this] } @@ -34,7 +34,7 @@ resource "aws_ec2_transit_gateway_route_table_propagation" "this" { provider = aws.hub count = local.create ? 1 : 0 transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.this[0].id - transit_gateway_route_table_id = var.transit_gateway_route_table_id + transit_gateway_route_table_id = local.transit_gateway_route_table_id depends_on = [aws_ec2_transit_gateway_vpc_attachment.this] } @@ -43,7 +43,7 @@ resource "aws_route" "this" { count = local.create ? length(data.aws_route_table.this[*].subnet_id) : 0 destination_cidr_block = var.destination_cidr_block - transit_gateway_id = var.transit_gateway_id + transit_gateway_id = local.transit_gateway_id route_table_id = sort(data.aws_route_table.this[*].route_table_id)[count.index] depends_on = [aws_ec2_transit_gateway_vpc_attachment.this] diff --git a/variables.tf b/variables.tf index 666fb02..53dba31 100644 --- a/variables.tf +++ b/variables.tf @@ -56,6 +56,7 @@ variable "transit_gateway_id" { variable "ram_resource_association_id" { description = "Identifier of the Resource Access Manager Resource Association" type = string + default = "" } variable "subnet_name_keyword_selector" { @@ -63,3 +64,9 @@ variable "subnet_name_keyword_selector" { type = string default = "private" } + +variable "transit_gateway_hub_name" { + description = "Name of the Transit Gateway to attach to" + type = string + default = "" +}